Skip to content

wip: txn forwarder liveness: mode-gate the sink and route by sender-affinity rendezvous - #136

Draft
bronxyz wants to merge 5 commits into
fix/stack-review-followupsfrom
feat/txn-forwarder-liveness
Draft

wip: txn forwarder liveness: mode-gate the sink and route by sender-affinity rendezvous#136
bronxyz wants to merge 5 commits into
fix/stack-review-followupsfrom
feat/txn-forwarder-liveness

Conversation

@bronxyz

@bronxyz bronxyz commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Work in progress, not part of the 1/9-9/9 txpool series: based on #135 only for the code it needs, and not to be merged with the stack. Four commits that keep the transaction forwarder live and trim hot-path costs.

  • Gate the forwarder sink on node mode (ae70095f): a non-batch-producing node (CvvInactive catching up, or an observer) pooled forwarded transactions and stranded them until rejoin. The worker now refuses the submit before the fan-out permit or any rayon work, reusing the existing error reply so there is no wire change. NodeMode moves to infrastructure types so worker and forwarder share one definition.
  • Route forwarding by sender-affinity rendezvous and per-validator health (5224badd): each sender's chain routes to a committee owner by rendezvous over the connected, closed-breaker validators, so every observer with the same view routes a sender identically (a load balancer can split a sender across observers but not across validators). A per-validator breaker opens on three consecutive transport failures or backlog sheds, immediately on a peer's mode reject, or on repeated non-inclusion (counted independently of send failures and closed only by an inclusion read off the node's own pool); a lapsed breaker admits one probe group at a time with a doubling hold, so a persistent censor is probed ever more rarely while an honest one recovers on its first probe. Re-sends re-select a live successor via rendezvous when a censor's breaker trips; the ring-walk remains the send-time failover. The AckedStale mark is dropped (a stale ack is stamped as a send, so it cannot silence a still-pending hash), the resend policy is latency-tuned (2-block anchor margin, 3s WAN budget, one backoff doubling), and in-flight marks are released by a 1s executed-hash pool lookup rather than the commit notification. The owner window keys on the executed consensus number (the canonical monotonic watermark), not the laggy recently-executed EVM tip.
  • Storage read-path efficiency and barrier-free cold finalize (4866118c): read the executed consensus tip and the worker max-seq scan through lazy read txns, touching only the rows needed instead of cloning/materializing the mem-resident table; project the tip's block/epoch/round from the raw ConsensusBlocks row instead of decoding every certificate's BLS signature; finalize a sealed cold segment without a writer barrier (the FIFO queue already applies index, prune, and high-water mark in order); open explicit write txns for one-off store writes; name vote tasks by header digest instead of full-header Debug. CertificateStore::write_all now borrows its certificates (impl Borrow<Certificate>), removing a full VecDeque<Certificate> clone on the cert-manager persist path and in the subscriber. Test doubles consolidate into one storage::test_utils::TestDb (behind a test-utils feature) so no crate re-implements the Database trait for tests.
  • Run the cert-manager high-water write off the runtime (ceedb6da): the ConsensusHeaders high-water store write moves to spawn_blocking with the join handle awaited, so an epoch-teardown abort drops the future at the await while the synchronous, backlog-paced write finishes on its own thread.

Surface areas touched

  • Consensus protocol (primary / worker / network / state-sync)
  • Execution / EVM
  • JSON-RPC (eth_*, rayls_*, faucet)
  • Middleware (orchestrator / processor / bridge)
  • Infrastructure (types / storage / config / network-cli)
  • On-chain contracts (rayls-contracts/)
  • Operations (etc/, scripts, Docker, compose)
  • CI / build (.github/workflows/, Makefile)
  • Documentation only (doc/, in-crate READMEs, root docs)
  • Tests only

Breaking / compatibility

None on the wire. The mode gate reuses the existing WorkerResponse::Error reply (the Display strings are the contract, kept distinct so a sender tells a mode reject from a load shed); un-upgraded senders map either case to walk-on. The forwarder routing, breaker, and resend policy are entirely sender-side and node-local; no message, codec, or gossip shape changed, so the range is rolling-upgrade safe. write_all's move to a borrowed parameter is an internal API change (all callers accept it, owned or borrowed). The owner window keys on the executed consensus number, a deterministic value every observer agrees on.

Testing

  • make check clean; make fmt clean; clippy adds no new warnings on the touched crates.
  • cargo nextest run -p rayls-middleware-orchestrator forwarder suite passes, covering the breaker state machine (transport/shed/mode-reject trips, non-inclusion trip closed only by inclusion), the half-open single-probe with doubling hold, the censoring-owner escape end-to-end, rendezvous stability/spread under eligibility changes, the slow-acking-owner-does-not-stall-others concurrency, and the latency-tuned resend gating.
  • cargo nextest run -p rayls-infrastructure-storage -p rayls-consensus-primary passes, including the barrier-free cold finalize oracle, the TestDb-driven cert-manager teardown-reap test, and the cert-store tests over the borrowed write_all (no VecDeque clone).
  • Reviewed under the domain lenses (narwhal-bullshark, tokio, idiomatic-rust, paradigm-engineer, tdd): no correctness defects; the health ranking and owner window are node-local and never enter a hashed, persisted, or networked path.

@bronxyz bronxyz changed the title feat/txn forwarder liveness feat: txn forwarder liveness: mode-gate the sink and rank by validator health Aug 27, 2026
@raylsnetwork raylsnetwork deleted a comment from github-actions Bot Aug 27, 2026
@bronxyz
bronxyz force-pushed the feat/txn-forwarder-liveness branch 2 times, most recently from bc7dd54 to ceedb6d Compare August 28, 2026 14:58
@raylsnetwork raylsnetwork deleted a comment from github-actions Bot Aug 28, 2026
@raylsnetwork raylsnetwork deleted a comment from github-actions Bot Aug 28, 2026
@bronxyz bronxyz changed the title feat: txn forwarder liveness: mode-gate the sink and rank by validator health feat: txn forwarder liveness: mode-gate the sink and route by sender-affinity rendezvous Aug 28, 2026
@bronxyz
bronxyz force-pushed the feat/txn-forwarder-liveness branch from ceedb6d to 0545fe4 Compare August 28, 2026 15:35
@raylsnetwork raylsnetwork deleted a comment from github-actions Bot Aug 28, 2026
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @bronxyz's task in 5m 41s —— View job


PR Review: feat/txn-forwarder-liveness

Tasks

  • Gather context and understand the PR
  • Read CLAUDE.md for repo guidelines (none found)
  • Review txn forwarder changes (health, routing, breaker logic)
  • Review storage / layered_db changes
  • Review in-flight marks changes
  • Review NodeMode / worker changes
  • Review cert-manager changes
  • Compile findings and post review

This is a well-engineered PR. The breaker state machine, rendezvous routing, and storage read-path changes are all solid. A few things worth addressing before merge:


Potential bugs / correctness

1. Latent panic in blame_non_inclusion with an empty committeehealth.rs:311

let peer = self.last_acked_by(sender).unwrap_or_else(|| committee[owner as usize]);

If committee is empty and last_acked_by returns None, this panics. The immediate caller (submit_to_committee) guards with !self.committee.is_empty(), but the function signature doesn't encode this invariant. Add a guard or document the precondition:

let peer = self.last_acked_by(sender)
    .or_else(|| committee.get(owner as usize).copied())?;  // return early if no peer to blame

Fix this →

2. Breaker::acked resets transport_failures but not not_includedhealth.rs:208-216

Self::Closed(closed) => {
    closed.transport_failures = 0;
    false
}

A validator that recovers from a send-failure trip (acked) still carries its previous not_included count. One more non-inclusion trips it immediately. This is documented as intentional ("only an inclusion does either"), but the asymmetry is subtle enough to warrant a comment on the Closed struct field, e.g. /// Only cleared by an inclusion; an ack does not clear it.

3. Two separate Instant::now() calls within submit_messagemod.rs:430, 464

let candidates = self.health.lock().candidates(owner, &self.committee, connected, now);
// ...
if self.health.lock().on_failure(&peer, &e, Instant::now()) {  // fresh Instant, not `now`

now is the tick's start (passed in from forward_once), while on_failure uses a fresh Instant::now() that could be several seconds later for a slow failover loop. The breaker is a heuristic, so correctness is unaffected, but the inconsistency means the hold duration is measured from actual failure time rather than tick start (which is actually correct for a retry timer). Consider making the intent explicit with a comment.


Performance

4. O(n²) connectivity filter in ValidatorHealth::candidateshealth.rs:377-385

.filter(|peer| connected.contains(peer))  // O(n) linear scan per peer

connected is a Vec<BlsPublicKey>. For committee sizes ≤ 30 this is negligible (the caller in submit_to_committee already collects connected into an Arc<Vec<_>>), but converting to a HashSet at the call site would make this O(n):

let connected_set: std::collections::HashSet<_> = connected.iter().collect();
.filter(|peer| connected_set.contains(peer))

Fix this →

5. walk_consensus_blocks_for_max_seq still decodes BLS signaturesworker.rs (acknowledged TODO)

The TODO: migrate to reverse_raw_iter... comment correctly identifies that decoding every certificate (including BLS signature decompression) dominates a cold whole-epoch scan. The executed_consensus_tip in handler.rs already solves this with ConsensusHeaderMeta::from_bytes — the same BcsCursor trick would work here. Not blocking, but the TODO should include an issue reference so it doesn't get lost.


Code quality / clarity

6. rendezvous_owner allocates a Vec<u8> key per sendermod.rs:576-585

let mut key = Vec::with_capacity(Address::len_bytes() + 8 + 96);

Called once in select_pending (blocking pool) and once in submit_to_committee per sender. For a pool with thousands of pending senders this is a lot of small allocations. A stack-allocated [u8; 128] with a prefix length would avoid this. Low priority given spawn_blocking isolation, but worth noting for future tuning.

7. aggregate_by_owner index access is uncheckedmod.rs:595

by_owner[group.owner as usize].extend(...)

Safe by construction (rendezvous_owner always returns < committee.len()), but a debug-mode panic with a confusing trace if the invariant breaks. A debug_assert!(group.owner < committee_size as u64) would make the invariant explicit.


Highlights (what works well)

  • ConsensusHeaderMeta::from_bytes skip chain — confirmed: Certificate::skipBlsSignature::skip → raw byte skip, no curve-point decompression. The claim "never touches the signature bytes" holds.
  • Breaker state machineCause::Send vs Cause::NotIncluded separation correctly models ack-doesn't-close-censor. The retrip doubling cap at 4 (max 16× cooldown) is a sensible bound.
  • admit_submit mode check before semaphore acquire — correctly avoids consuming a backlog permit for a non-producing node.
  • cert_manager spawn_blocking pattern — the JoinError mapping and double-? at cert_manager.rs:308-309 are correct; the await is the clean epoch-abort yield point.
  • Wire contract testsubmit_rejection_replies_round_trip_and_stay_distinct pins the exact Display strings, which is the right way to guard a string-parsed protocol contract.
  • write_all borrowing — removing the VecDeque<Certificate> clone on the cert-manager persist path is a clean improvement.
  • Barrier-free cold finalize — the FIFO ordering argument holds; the test live_finalize_issues_no_writer_barrier is a good regression guard.

None of the issues are blocking correctness for normal operation. Items 1–2 are the ones worth fixing before merge; items 3–7 are polish/future-hardening.

@github-actions

Copy link
Copy Markdown
Contributor

Security Scan - Code

Severity: HIGH, CRITICAL

No vulnerabilities found

View scan results

Report Summary

┌─────────────────────────────────────────────┬────────────┬─────────────────┬───────────────────┐
│                   Target                    │    Type    │ Vulnerabilities │ Misconfigurations │
├─────────────────────────────────────────────┼────────────┼─────────────────┼───────────────────┤
│ Cargo.lock                                  │   cargo    │        0        │         -         │
├─────────────────────────────────────────────┼────────────┼─────────────────┼───────────────────┤
│ crates/testing/fuzz-targets/Cargo.lock      │   cargo    │        0        │         -         │
├─────────────────────────────────────────────┼────────────┼─────────────────┼───────────────────┤
│ crates/testing/fuzz-targets/fuzz/Cargo.lock │   cargo    │        0        │         -         │
├─────────────────────────────────────────────┼────────────┼─────────────────┼───────────────────┤
│ etc/state-sum/Cargo.lock                    │   cargo    │        0        │         -         │
├─────────────────────────────────────────────┼────────────┼─────────────────┼───────────────────┤
│ etc/tps/package-lock.json                   │    npm     │        0        │         -         │
├─────────────────────────────────────────────┼────────────┼─────────────────┼───────────────────┤
│ rayls-contracts/package-lock.json           │    npm     │        0        │         -         │
├─────────────────────────────────────────────┼────────────┼─────────────────┼───────────────────┤
│ etc/chaos-network/Dockerfile                │ dockerfile │        -        │         0         │
├─────────────────────────────────────────────┼────────────┼─────────────────┼───────────────────┤
│ etc/docker-network/Dockerfile               │ dockerfile │        -        │         0         │
├─────────────────────────────────────────────┼────────────┼─────────────────┼───────────────────┤
│ etc/docker-replay/Dockerfile                │ dockerfile │        -        │         0         │
└─────────────────────────────────────────────┴────────────┴─────────────────┴───────────────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)

@bronxyz bronxyz changed the title feat: txn forwarder liveness: mode-gate the sink and route by sender-affinity rendezvous wip: txn forwarder liveness: mode-gate the sink and route by sender-affinity rendezvous Aug 28, 2026
- a non-batch-producing node (CvvInactive catching up, or an observer) pooled
  forwarded transactions and stranded them until rejoin; the worker now refuses
  the submit before the fan-out permit or any rayon work
- refusals reuse the existing error reply, so there is no wire change: the
  Display strings are the contract, kept distinct so a sender can tell a mode
  reject from a load shed, and un-upgraded senders already map either to walk-on
- submit_txns parses the reply once, beside the Display it inverts, into
  SubmitError { Rejected(SubmitRejection), Network }, so a sender matches an enum
  instead of re-classifying strings
- the mode reaches the worker as a fresh receiver of the app-scoped node_mode
  watch at each epoch spawn, so a mid-epoch demotion gates the next submit
- move NodeMode to infrastructure types so the worker and forwarder share one
  definition
…dator health

- route each sender's chain to a committee owner by rendezvous over the connected, closed-breaker validators, so every observer with the same view routes a sender identically: a load balancer can split a sender across observers but not across validators
- the only ranking signal is this node's own submit outcomes: no bus field, gossip hook, or app-scoped store, and no reputation or leader-schedule input that would starve slow but honest geo-remote validators
- exclude a tripped validator from the rendezvous eligibility set, so a re-send re-selects a live successor instead of retrying a censored owner; within one send, fail over along the validator ring from the owner slot (held breakers last) when the chosen owner is momentarily unreachable
- open a breaker on three consecutive transport failures or backlog sheds, immediately on a peer's mode reject, or on repeated non-inclusion (counted independently of send failures); close it on an ack or, for a non-inclusion trip, only on an inclusion read off the node's own pool
- once a hold lapses admit one probe group at a time (half-open) and double the hold on each failed probe up to a cap, so a persistent censor is probed ever more rarely while an honest one closes on its first probe's inclusion
- the breaker is a closed/open sum type whose open state only a trip can construct, so a hold cannot be forged and half-open is read off the clock; timing is deliberate wall-clock, a node-local heuristic, never a replicated decision
- key the owner window on the executed consensus number (monotonic per committed output), the same watermark the resend policy anchors on, rather than the laggy recently-executed EVM tip
- drop the AckedStale terminal mark: a stale ack is stamped as a send, so a hash still pending after the window re-sends and an ack cannot silence a transaction
- resend a forwarded tx once local execution passes its send head by a 2-block margin within a 3s WAN latency budget, with backoff capped at one doubling so a frontier the owner acked but has not included is not throttled out of the budget
- release in-flight marks by looking up the node's executed hashes against the pool on a 1s tick, not on the commit notification, so the builder never re-seals an executed transaction and pool-lock time does not grow with blocks per second

- carry the re-send flag off the existing ForwardProbe.forwarded bit rather than a separate count, and own blame-on-non-inclusion inside ValidatorHealth
- read the executed consensus tip and the worker max-seq scan through read txns, whose iterators are lazy, so each touches only the rows it needs instead of the store-level iterator cloning or materializing the whole mem-resident table
- project the tip's block/epoch/round from the raw ConsensusBlocks row instead of decoding the sub-dag, which would reconstruct every certificate's BLS signature on each gossiped certificate
- finalize a sealed cold segment without a writer barrier, since the finalize already re-waits every queued consensus write in FIFO order
- merge the two hot-layer tips lazily in last_record and record_prior_to, scanning the merged iterators only when the persisted tip is tombstoned or both layers are empty
- open explicit write txns for one-off store writes (kad, the batch/payload/proposer/vote stores, epoch state) instead of the implicit per-call txn
- name vote tasks by header digest computed once above the per-peer loop, instead of rendering the whole header's Debug on every per-peer task spawn

- gate storage test rigs behind a new test-utils feature and add a transaction-scoped iterator-ordering test
- move the ConsensusHeaders high-water store write to spawn_blocking and await the join handle, so an epoch-teardown abort drops this future at the await while the synchronous write finishes on its own thread
- the write paces every insert under the mem-store lock behind a writer backlog; run inline, a worker parked inside it pins the future on its stack where the abort can never reach it
- await the manager's forwards in the processing test, since the store write now completes on the blocking pool before the forward rather than inline
- the validator that acked a stream's previous chunk leads the next one, so a stream that
  failed over stays whole on one pool
- a per-chunk walk otherwise splits one sender's nonce chain between a flapping owner and its
  successor, each half gapped and queued until the other executes
@bronxyz
bronxyz force-pushed the feat/txn-forwarder-liveness branch from 0545fe4 to 2477780 Compare August 28, 2026 17:01
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @bronxyz's task in 4m 13s —— View job


PR Review: feat/txn-forwarder-liveness (re-review after sync)

Tasks

  • Read health.rs (breaker state machine)
  • Read txn_forwarder/mod.rs (routing/rendezvous)
  • Read handler.rs (ConsensusHeaderMeta skip chain)
  • Read layered_db.rs (read-path efficiency)
  • Read cert_manager.rs (spawn_blocking pattern)
  • Read tests.rs (test coverage)
  • Compile findings and post review

This is a well-engineered set of changes. The breaker state machine, rendezvous routing, and storage read-path work are all solid. The prior review identified the right blocking items; they remain unfixed in the current state. A few new observations follow.


Still unfixed from prior review

1. Latent panic in blame_non_inclusion with an empty committeehealth.rs:311

let peer = self.last_acked_by(sender).unwrap_or_else(|| committee[owner as usize]);

submit_to_committee guards with !self.committee.is_empty() before calling here, so this cannot panic in production today. But the function's signature carries no precondition, and the owner in the fallback path comes from select_pending where an empty-committee rendezvous_owner returns Noneunwrap_or(0), so a future caller without the guard would index committee[0] on an empty slice. A committee.get(owner as usize).copied()? early-return or a debug_assert!(!committee.is_empty()) makes the invariant explicit.

Fix this →

2. Breaker::acked resets transport_failures but not not_includedhealth.rs:208-216

Self::Closed(closed) => {
    closed.transport_failures = 0;  // not_included untouched
    false
}

Confirmed intentional ("only an inclusion does either"), but a reader hitting this cold will assume an ack clears the whole record. The Closed::not_included field deserves a doc comment: /// Only cleared by an inclusion; an ack does not reset it.

Fix this →


New findings

3. Breaker::failed silently refreshes the hold of a holding breakerhealth.rs:181

(Self::Open(open), _) => Self::Open(Open { since: now, ..open }),

This arm fires when the breaker is holding AND a failed send arrives (last-resort path). Setting since = now restarts the hold from that moment — retrips stays unchanged, so the hold duration doesn't grow, but it is indefinitely refreshed while the last-resort sends keep failing. The comment acknowledges "restarts the same hold," but the behavior is that a persistently unreachable validator holding position behind every live one can hold the forwarder's last-resort slot occupied indefinitely. Worth a note: if all candidates are Open and the walk exhausts without a success, the refresh happens on every tick. The warn!("no live validator accepted the message") log is the signal, but the hold refresh deserves a comment alongside it.

4. Breaker::acked does not modify an Open(NotIncluded) breakerhealth.rs:214

Self::Open(open) => matches!(open.cause, Cause::Send),

For a NotIncluded open breaker, acked returns false and leaves open.probe untouched. The probe's sender and at fields continue to gate admission. This is correct — a censor acks everything, so an ack proves nothing — but the &mut self signature is misleading on this arm since it makes no mutation. A comment on the NotIncluded arm (// ack is not evidence; probe and hold stay intact) would clarify the intent for the next reader.

5. submit_message has two semantically distinct Instant::now() callsmod.rs:438, 469, 476

let mut candidates = self.health.lock().candidates(owner, &self.committee, connected, now);
// ...  (inside the failover loop)
self.marks.mark_forwarded(hashes, Instant::now(), anchor);  // line 469 — ack time, correct
self.health.lock().on_failure(&peer, &e, Instant::now());   // line 476 — failure time, correct

now (the tick's start, passed in from forward_once) is used for the candidates call; fresh Instant::now() calls are used for stamping the forwarding mark and for the breaker failure record. Both fresh calls are correct for their purpose — the mark should carry the actual ack time, and the breaker hold should run from the actual failure time. The only risk is a future maintainer collapsing the three into now uniformly. A short inline comment on each fresh call ("ack time, not tick start") would prevent that.

6. aggregate_by_owner index is uncheckedmod.rs:615

by_owner[group.owner as usize].extend(Self::nonce_sorted(group.txns));

Safe by construction: rendezvous_owner returns Some(slot) where slot < committee.len(), and aggregate_by_owner allocates committee_size.max(1) slots. The owner as usize cast is safe because u64 slots returned by rendezvous_owner fit in usize for any real committee. A debug_assert!(group.owner < committee_size as u64) would surface invariant breaks in dev/test without any release cost.

7. record_prior_to on LayeredDatabase opens a fresh read_txn every calllayered_db.rs:1490-1492

fn record_prior_to<T: Table>(&self, key: &T::Key) -> Option<(T::Key, T::Value)> {
    self.read_txn().ok()?.record_prior_to::<T>(key)
}

read_txn opens both a MemDbTx and a persistent DB read transaction. If LayeredDbTx::record_prior_to's tip shortcut (merge_tips) misses and falls back to iter().take_while(...).last(), that's an O(n) mem snapshot each call. The reverse_skip_to method exists precisely to avoid this — a single positioned seek backwards. The record_prior_to forwarding wrapper is fine for the sparse usage it sees today, but a comment pointing to reverse_skip_to would help future callers that land on performance-sensitive paths.

8. BehindTracker::record_behind wall-clock usagehandler.rs:84

if self.last_progress_at.elapsed() >= window {

Instant::now() is fine (the tracker is a local heuristic, not consensus state). The BEHIND_STALL_WINDOW of 5s is noted as "~5 rounds under load" — worth checking whether a loaded testnet can produce 5 rounds without a commit advancing in edge cases (e.g. leader rotation taking 2 rounds), which would cause a false-positive demotion. Not blocking, but the 5s constant is the knob to tune if false demotion reports come in.


Corrections to prior review


Highlights (confirmed correct)

  • Breaker::hold() overflow1 << retrips.min(4) gives max 16×; BREAKER_COOLDOWN * 16 = 160s. No overflow.
  • is_caught_up underflowseen >= anchor is checked before seen - anchor. Safe.
  • chunk_under_budget oversized solo — The !message.is_empty() guard before flushing ensures a single oversized transaction goes out alone rather than being dropped.
  • cert_manager spawn_blocking double-?map_err(|_| JoinError)?? propagates JoinError and write error correctly; the await is the clean epoch-abort yield point.
  • executed_consensus_tip raw skipwith_read_txnreverse_raw_iter.next() touches one row; ConsensusHeaderMeta::from_bytes skips BLS decompression entirely. Claim verified.
  • Rendezvous lock discipline — The health lock in submit_to_committee is held for the entire owner-selection loop but dropped before every await. Confirmed correct per the "never held across an await" contract.
  • acked correctly removes the breaker for Cause::Sendon_success calls breakers.remove(peer) when acked() returns true, which correctly clears open.probe as a side-effect of the removal. The &mut self signature has no mutation in the Open arm, but the map removal in the caller achieves the reset.
  • Wire-contract testsubmit_rejection_replies_round_trip_and_stay_distinct pins Display strings. Correct way to guard a string-parsed protocol contract.
  • write_all borrowing — The impl Borrow<Certificate> parameter removes the VecDeque<Certificate> clone on the cert-manager persist path. Clean.

Items 1–2 are worth fixing before merge. Items 3–8 are clarity/future-hardening and are non-blocking.

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.

1 participant