Skip to content

Commit de64b47

Browse files
committed
[tesseract]: greedily select rotation proofs the destination can verify
A stored rotation proof keyed K is a BEEFY commitment signed by the outgoing set (validatorSetId = K-1) whose leaf reveals K+1. A destination only accepts a commitment signed by its on-chain current or next set, so a destination that has skipped a set (next > current+1, e.g. BSC current=5071/next=5073) cannot verify keyed-(next) — it is signed by next-1, which it never saw — and reverts with UnknownAuthoritySet. Replace the single-offset anchor with a greedy walk of the destination state: applying keyed-K to {cur,nxt} yields {nxt,K+1}, so pick keyed-(cur+1) when contiguous (advance by one, no skip) and keyed-(nxt+1) when gapped (signed by nxt; advances, stepping over sets that can no longer be installed). Stop when the next needed proof hasn't been produced on HB yet and continue on a later tick. rotation_proofs_from now returns all stored proofs above `from` and no longer errors on a missing immediate-next entry; the caller decides what it needs.
1 parent 1c615c9 commit de64b47

2 files changed

Lines changed: 50 additions & 29 deletions

File tree

tesseract/messaging/messaging/src/outbound.rs

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -712,33 +712,57 @@ async fn catch_up_rotations(
712712
return Ok(Vec::new());
713713
}
714714

715-
// Anchor the catch-up on the destination's on-chain `next` set id, not its `current`.
716-
// A destination can only verify a consensus proof signed by an authority set it holds
717-
// on-chain — its `current` or its `next` set. When it has skipped intermediate sets
718-
// (its `next` is more than one ahead of `current`, e.g. current=5071/next=5073 with
719-
// 5072 never installed because an applied proof's MMR leaf jumped the next-set id), a
720-
// rotation proof for one of those skipped sets (5072) is signed by an authority set the
721-
// destination has never seen and reverts on-chain with `UnknownAuthoritySet`. Fetching
722-
// rotations strictly above `dest_next_id - 1` yields only set ids `>= dest_next_id`, so
723-
// the first proof we submit is the one signed by the destination's `next` set — the
724-
// smallest set id it can actually accept. In the common contiguous case
725-
// (`next == current + 1`) this is identical to anchoring on `current`.
726-
let anchor = dest_next_id.saturating_sub(1).max(dest_set_id);
727-
let rotations: Vec<RotationProof> = proof_source
728-
.rotation_proofs_from(anchor)
715+
// Greedily walk the destination forward, picking only rotation proofs it can actually verify
716+
// and that advance its state. A stored rotation proof keyed `K` is a BEEFY commitment signed
717+
// by `validatorSetId = K - 1` (the outgoing set) whose MMR leaf reveals the next set `K + 1`.
718+
// The destination's verifier accepts a commitment only if its `validatorSetId` is the
719+
// destination's on-chain `current` or `next` set, and it rotates (`current <- next`,
720+
// `next <- leaf.next`) only when the leaf is beyond the current `next`.
721+
//
722+
// Applying keyed-`K` to `{cur, nxt}` (which needs `K-1 ∈ {cur, nxt}` and `K+1 > nxt`) yields
723+
// `{nxt, K+1}`. So from a contiguous state we pick keyed-`cur+1` (signed by `cur`) and stay
724+
// contiguous — no authority set is skipped. From a state that has already leap-frogged a set
725+
// (`nxt > cur + 1`, e.g. BSC at current=5071/next=5073) the only acceptable advancing proof is
726+
// keyed-`nxt+1` (signed by `nxt`); the skipped sets can never be re-installed, so progress
727+
// there steps over them. Submitting keyed-`nxt` (signed by `nxt-1`) — the previous anchor —
728+
// reverts with `UnknownAuthoritySet` for such a destination, which is the bug this replaces.
729+
let by_set_id: BTreeMap<u64, RotationProof> = proof_source
730+
.rotation_proofs_from(dest_set_id)
729731
.await
730-
.map_err(|err| anyhow::anyhow!("rotation_proofs_from({anchor}): {err:?}"))?;
731-
if rotations.is_empty() {
732-
return Err(anyhow!("dest is lagging but no rotation proofs are cached on HB"));
732+
.map_err(|err| anyhow::anyhow!("rotation_proofs_from({dest_set_id}): {err:?}"))?
733+
.into_iter()
734+
.map(|r| (r.set_id, r))
735+
.collect();
736+
737+
let mut cur = dest_set_id;
738+
let mut nxt = dest_next_id;
739+
let mut rotations: Vec<RotationProof> = Vec::new();
740+
while cur < hb_set_id {
741+
// Contiguous → advance by one (no skip); gapped → advance to `nxt` (forced skip).
742+
let key = if nxt == cur + 1 { cur + 1 } else { nxt + 1 };
743+
let Some(proof) = by_set_id.get(&key) else {
744+
// The next proof the destination needs hasn't been produced on HB yet (HB hasn't
745+
// rotated that far). Stop; a later tick continues once it's available.
746+
break;
747+
};
748+
// The main batch delivers `current_set_id` itself via its `consensus_msg`, so don't
749+
// double-submit it — but the destination still advances past it either way.
750+
if Some(key) != current_set_id {
751+
rotations.push(proof.clone());
752+
}
753+
cur = nxt;
754+
nxt = key + 1;
733755
}
734756

735-
// Drop the rotation that matches the current notification's set_id —
736-
// the main batch will submit that one via its `consensus_msg`. Without
737-
// this filter the same proof would land twice on the destination: once
738-
// here, once via the main batch.
739-
let rotations: Vec<RotationProof> =
740-
rotations.into_iter().filter(|r| Some(r.set_id) != current_set_id).collect();
741757
if rotations.is_empty() {
758+
tracing::debug!(
759+
target: LOG_TARGET,
760+
dest = %dest_name,
761+
dest_set_id,
762+
dest_next_id,
763+
hb_set_id,
764+
"dest is behind but no applicable rotation proof is available yet; waiting",
765+
);
742766
return Ok(Vec::new());
743767
}
744768

tesseract/relayer/src/provider.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,9 @@ impl ConsensusProofSource for OffchainProofSource {
105105
let map: BTreeMap<u64, u64> = Decode::decode(&mut &bytes[..])
106106
.map_err(|err| anyhow!("decode RotationProofs BTreeMap: {err:?}"))?;
107107

108-
let next_set_id = from_set_id + 1;
109-
if !map.contains_key(&next_set_id) {
110-
return Err(anyhow!(
111-
"RotationProofs missing entry for set_id {next_set_id} (catching up from {from_set_id})"
112-
));
113-
}
108+
// Return whatever is stored above `from_set_id`; the caller greedily selects the subset
109+
// the destination can actually verify and apply. A missing immediate-next entry is not
110+
// fatal here — the caller decides whether the keys it needs are present.
114111

115112
// Walk ascending so the caller can submit rotations in order.
116113
let mut out = Vec::new();

0 commit comments

Comments
 (0)