Skip to main content

auths_id/keri/
sync.rs

1//! Validated KEL merge between registries — the trust core of registry
2//! propagation (`registry push` / `registry pull`).
3//!
4//! Transport-free: both sides are [`RegistryBackend`] ports; the git fetch /
5//! push that produces the source snapshot lives in the storage adapter. The
6//! destination registry is the **trusted floor** (local-first): a source KEL
7//! may only *advance* it, never rewrite it. Per identity in the source:
8//!
9//! - **prefix-binding guard** — the served inception must re-derive the
10//!   claimed prefix ([`verify_prefix_binding`]), so a source cannot
11//!   substitute a different identity's KEL;
12//! - **authenticated replay** — every event must carry a valid signature from
13//!   the in-force key-state ([`validate_signed_kel`]); delegated (`dip`/`drt`)
14//!   events resolve their anchoring seal from the source, then the
15//!   destination. A KEL the source cannot prove it signed is refused whole;
16//! - **rollback floor** — a source tip at-or-behind the destination's changes
17//!   nothing (prefer local; an older remote is never an instruction to
18//!   forget);
19//! - **fork refusal** — a same-sequence SAID divergence between source and
20//!   destination fails the merge loudly ([`RegistryMergeError::Forked`])
21//!   rather than silently picking a side.
22//!
23//! Only after all four hold is the strictly-newer suffix appended —
24//! signature attachments included, so the destination's KELs remain
25//! authenticatable (never merely structurally replayable).
26
27use std::collections::{BTreeSet, HashSet};
28use std::ops::ControlFlow;
29
30use auths_keri::{
31    Acdc, DelegatorKelLookup, KelSealIndex, Prefix, Said, SignedEvent, SourceSeal, TelEvent,
32    parse_delegated_attachment, validate_signed_kel, validate_tel,
33};
34
35use super::kel_resolver::{KelResolveError, collect_kel_capped, verify_prefix_binding};
36use crate::ports::registry::{RegistryBackend, RegistryError};
37
38/// DoS bounds applied when reading a KEL out of an untrusted source registry.
39#[derive(Debug, Clone, Copy)]
40pub struct KelCaps {
41    /// Hard cap on event count per KEL.
42    pub max_events: usize,
43    /// Hard cap on total serialized size per KEL, in bytes.
44    pub max_bytes: usize,
45}
46
47/// What the merge did for one identity's KEL.
48#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
49#[serde(rename_all = "snake_case", tag = "outcome")]
50pub enum MergeOutcome {
51    /// The destination had no KEL for this prefix — the full authenticated
52    /// KEL was imported.
53    Imported {
54        /// Number of events written.
55        events: usize,
56    },
57    /// The destination's KEL was behind — the strictly-newer authenticated
58    /// suffix was appended.
59    Advanced {
60        /// Number of events appended.
61        events: usize,
62    },
63    /// The destination already holds everything the source offered
64    /// (equal or newer). Nothing was written.
65    AlreadyCurrent,
66}
67
68/// Per-identity merge report.
69#[derive(Debug, Clone, serde::Serialize)]
70pub struct MergedKel {
71    /// The identity prefix.
72    pub prefix: Prefix,
73    /// What happened to its KEL in the destination.
74    #[serde(flatten)]
75    pub outcome: MergeOutcome,
76}
77
78/// Why a registry merge was refused. Any error aborts the whole merge — a
79/// pull never partially trusts a source that failed one identity's checks.
80#[derive(Debug, thiserror::Error)]
81#[non_exhaustive]
82pub enum RegistryMergeError {
83    /// The source listed an identity whose id is not a valid prefix.
84    #[error("source registry lists an invalid identity prefix '{id}': {reason}")]
85    InvalidPrefix {
86        /// The offending identity id.
87        id: String,
88        /// Why it failed to parse.
89        reason: String,
90    },
91
92    /// Reading the source KEL failed the untrusted-read guards
93    /// (not-found / oversized / truncated / prefix-binding).
94    #[error("source KEL for {prefix} was refused: {source}")]
95    SourceKel {
96        /// The identity prefix.
97        prefix: Prefix,
98        /// The guard that refused it.
99        #[source]
100        source: KelResolveError,
101    },
102
103    /// An event in the source KEL has no signature attachment — it cannot be
104    /// authenticated, so it is never persisted.
105    #[error("source KEL for {prefix} has no signature attachment at sequence {sequence}")]
106    MissingSignature {
107        /// The identity prefix.
108        prefix: Prefix,
109        /// The unsigned event's sequence.
110        sequence: u128,
111    },
112
113    /// The source KEL failed authenticated replay (bad signature, broken
114    /// chain, unauthorized delegation, …).
115    #[error("source KEL for {prefix} failed authentication: {reason}")]
116    Unauthenticated {
117        /// The identity prefix.
118        prefix: Prefix,
119        /// The validation failure.
120        reason: String,
121    },
122
123    /// Source and destination disagree at a shared sequence — a fork.
124    /// Refused outright; a merge never picks a side of a fork.
125    #[error(
126        "KEL fork for {prefix} at sequence {sequence}: \
127         destination has {destination}, source has {incoming}"
128    )]
129    Forked {
130        /// The identity prefix.
131        prefix: Prefix,
132        /// The diverging sequence number.
133        sequence: u128,
134        /// The destination's event SAID at that sequence.
135        destination: Said,
136        /// The source's event SAID at that sequence.
137        incoming: Said,
138    },
139
140    /// A backend read/write failed.
141    #[error("registry backend error: {0}")]
142    Storage(#[from] RegistryError),
143
144    /// A source credential blob did not parse as a stored ACDC envelope, or its
145    /// recomputed SAID did not match the SAID it was filed under — a tampered or
146    /// malformed credential body is refused rather than copied onto the cold
147    /// machine.
148    #[error("source credential {credential} under {issuer} was refused: {reason}")]
149    CredentialRefused {
150        /// The issuer the credential was filed under.
151        issuer: Prefix,
152        /// The credential SAID (the on-disk filename).
153        credential: Said,
154        /// Why it was refused.
155        reason: String,
156    },
157
158    /// A source credential names an issuer whose KEL was not authenticated in
159    /// this merge — a credential with no anchoring identity is never imported.
160    #[error("source credential {credential} names unknown issuer {issuer} (no authenticated KEL)")]
161    CredentialOrphan {
162        /// The unanchored issuer.
163        issuer: Prefix,
164        /// The orphaned credential SAID.
165        credential: Said,
166    },
167
168    /// A source TEL chain failed structural validation (a recomputed event SAID
169    /// did not match, or the `vcp → iss… → rev…` shape was broken) — a tampered
170    /// TEL is refused rather than copied.
171    #[error("source TEL {credential} under {issuer}/{registry} was refused: {reason}")]
172    TelRefused {
173        /// The issuer the TEL was filed under.
174        issuer: Prefix,
175        /// The registry SAID.
176        registry: Said,
177        /// The credential SAID the TEL belongs to.
178        credential: Said,
179        /// Why it was refused.
180        reason: String,
181    },
182}
183
184/// Merge every identity's KEL from `source` into `dest` under the guards
185/// documented at the module level.
186///
187/// `source` is untrusted (a fetched snapshot); `dest` is the local trusted
188/// floor. Returns one [`MergedKel`] per source identity, in prefix order.
189/// Any guard failure aborts the whole merge with the first error.
190///
191/// Args:
192/// * `source`: The untrusted registry to read from.
193/// * `dest`: The trusted local registry to advance.
194/// * `caps`: DoS bounds for reading the source.
195///
196/// Usage:
197/// ```ignore
198/// let report = merge_registries(snapshot.backend(), &local, &caps)?;
199/// ```
200pub fn merge_registries(
201    source: &dyn RegistryBackend,
202    dest: &dyn RegistryBackend,
203    caps: &KelCaps,
204) -> Result<Vec<MergedKel>, RegistryMergeError> {
205    let mut ids: Vec<String> = Vec::new();
206    source.visit_identities(&mut |id| {
207        ids.push(id.to_string());
208        ControlFlow::Continue(())
209    })?;
210    ids.sort();
211    ids.dedup();
212
213    let mut report = Vec::with_capacity(ids.len());
214    for id in ids {
215        let prefix = Prefix::new(id.clone()).map_err(|e| RegistryMergeError::InvalidPrefix {
216            id,
217            reason: e.to_string(),
218        })?;
219        let outcome = merge_kel(source, dest, &prefix, caps)?;
220        report.push(MergedKel { prefix, outcome });
221    }
222    Ok(report)
223}
224
225/// Merge one identity's KEL from `source` into `dest`.
226fn merge_kel(
227    source: &dyn RegistryBackend,
228    dest: &dyn RegistryBackend,
229    prefix: &Prefix,
230    caps: &KelCaps,
231) -> Result<MergeOutcome, RegistryMergeError> {
232    let refused = |source: KelResolveError| RegistryMergeError::SourceKel {
233        prefix: prefix.clone(),
234        source,
235    };
236
237    let events =
238        collect_kel_capped(source, prefix, caps.max_events, caps.max_bytes).map_err(refused)?;
239    verify_prefix_binding(prefix, &events).map_err(refused)?;
240
241    // Pair every event with its signature attachment; an unsigned event is
242    // unauthenticatable and refused before any validation work.
243    let mut attachments = Vec::with_capacity(events.len());
244    let mut signed = Vec::with_capacity(events.len());
245    for event in &events {
246        let sequence = event.sequence().value();
247        let attachment = source.get_attachment(prefix, sequence)?.ok_or_else(|| {
248            RegistryMergeError::MissingSignature {
249                prefix: prefix.clone(),
250                sequence,
251            }
252        })?;
253        let (sigs, _seals) = parse_delegated_attachment(&attachment).map_err(|e| {
254            RegistryMergeError::Unauthenticated {
255                prefix: prefix.clone(),
256                reason: format!("unparseable attachment at sequence {sequence}: {e}"),
257            }
258        })?;
259        // Delegated events read from a registry backend already carry their
260        // rehydrated source seal; the attachment's `-G` group is redundant here.
261        signed.push(SignedEvent::new(event.clone(), sigs));
262        attachments.push(attachment);
263    }
264
265    let lookup = BackendSealLookup {
266        backends: [source, dest],
267        caps: *caps,
268    };
269    validate_signed_kel(&signed, Some(&lookup)).map_err(|e| {
270        RegistryMergeError::Unauthenticated {
271            prefix: prefix.clone(),
272            reason: e.to_string(),
273        }
274    })?;
275
276    let dest_tip = match dest.get_tip(prefix) {
277        Ok(tip) => Some(tip.sequence),
278        Err(RegistryError::NotFound { .. }) => None,
279        Err(e) => return Err(e.into()),
280    };
281
282    match dest_tip {
283        None => {
284            for (event, attachment) in events.iter().zip(&attachments) {
285                dest.append_signed_event(prefix, event, attachment)?;
286            }
287            Ok(MergeOutcome::Imported {
288                events: events.len(),
289            })
290        }
291        Some(dest_tip) => {
292            // Fork refusal: every event in the shared range must agree by SAID.
293            for event in events.iter().filter(|e| e.sequence().value() <= dest_tip) {
294                let sequence = event.sequence().value();
295                let local = dest.get_event(prefix, sequence)?;
296                if local.said() != event.said() {
297                    return Err(RegistryMergeError::Forked {
298                        prefix: prefix.clone(),
299                        sequence,
300                        destination: local.said().clone(),
301                        incoming: event.said().clone(),
302                    });
303                }
304            }
305            let newer: Vec<_> = events
306                .iter()
307                .zip(&attachments)
308                .filter(|(event, _)| event.sequence().value() > dest_tip)
309                .collect();
310            if newer.is_empty() {
311                return Ok(MergeOutcome::AlreadyCurrent);
312            }
313            let appended = newer.len();
314            for (event, attachment) in newer {
315                dest.append_signed_event(prefix, event, attachment)?;
316            }
317            Ok(MergeOutcome::Advanced { events: appended })
318        }
319    }
320}
321
322/// What the credential + TEL merge did, counted over the whole registry.
323///
324/// The KEL merge ([`merge_registries`]) is the trust core — it authenticates
325/// every imported event. This report covers the *artifact* layer that rides on
326/// top of those authenticated KELs: the ACDC credential bodies and their TEL
327/// chains, which a cold machine needs to re-verify a credential end-to-end.
328#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
329pub struct MergedCredentials {
330    /// Credential bodies newly written to the destination.
331    pub credentials_imported: usize,
332    /// Credential bodies the destination already held (idempotent skip).
333    pub credentials_already_present: usize,
334    /// TEL events newly written to the destination.
335    pub tel_events_imported: usize,
336    /// TEL events the destination already held (idempotent skip).
337    pub tel_events_already_present: usize,
338}
339
340/// Merge every credential body and TEL chain from `source` into `dest`.
341///
342/// Runs AFTER [`merge_registries`] has authenticated the KELs: the credential
343/// and TEL artifacts ride on those KELs and are re-verified at
344/// `credential verify` time (the issuer's detached signature is checked against
345/// the authenticated key-state; the TEL anchors are checked against the KEL).
346/// This step only *materializes* the bodies so a cold machine has them to verify.
347///
348/// It is still fail-closed, mirroring the KEL merge's discipline:
349///
350/// - **anchored-issuer only** — a credential whose issuer has no authenticated
351///   KEL (not in `authenticated_issuers`) is refused
352///   ([`RegistryMergeError::CredentialOrphan`]); a dangling credential is never
353///   imported;
354/// - **content-address consistency** — a credential body must parse and
355///   recompute to the SAID it is filed under, and name that issuer
356///   ([`RegistryMergeError::CredentialRefused`]); a byte-flipped credential is
357///   refused, not copied;
358/// - **TEL structural validity** — a TEL chain must pass [`validate_tel`]
359///   (every event's recomputed SAID matches; the `vcp → iss… → rev…` shape
360///   holds) before any event is written ([`RegistryMergeError::TelRefused`]); a
361///   byte-flipped TEL event is refused.
362///
363/// Writes are idempotent: a credential body / TEL event the destination already
364/// holds (by SAID-addressed path) is skipped, so a re-pull changes nothing.
365/// Any refusal aborts the whole step — a pull never partially trusts a source.
366///
367/// Args:
368/// * `source`: The untrusted registry snapshot to read artifacts from.
369/// * `dest`: The trusted local registry to import into.
370/// * `authenticated_issuers`: Prefixes whose KELs `merge_registries` authenticated.
371///
372/// Usage:
373/// ```ignore
374/// let kels = merge_registries(src, dst, &caps)?;
375/// let issuers = kels.iter().map(|m| m.prefix.clone()).collect();
376/// let creds = merge_credentials_and_tel(src, dst, &issuers)?;
377/// ```
378pub fn merge_credentials_and_tel(
379    source: &dyn RegistryBackend,
380    dest: &dyn RegistryBackend,
381    authenticated_issuers: &HashSet<Prefix>,
382) -> Result<MergedCredentials, RegistryMergeError> {
383    let mut report = MergedCredentials::default();
384    merge_credential_bodies(source, dest, authenticated_issuers, &mut report)?;
385    merge_tel_chains(source, dest, authenticated_issuers, &mut report)?;
386    Ok(report)
387}
388
389/// Import every source credential body for an authenticated issuer (the first
390/// leg of [`merge_credentials_and_tel`]).
391fn merge_credential_bodies(
392    source: &dyn RegistryBackend,
393    dest: &dyn RegistryBackend,
394    authenticated_issuers: &HashSet<Prefix>,
395    report: &mut MergedCredentials,
396) -> Result<(), RegistryMergeError> {
397    let mut credentials: Vec<(Prefix, Said, Vec<u8>)> = Vec::new();
398    source.visit_credentials(&mut |issuer, credential, bytes| {
399        credentials.push((issuer.clone(), credential.clone(), bytes.to_vec()));
400        ControlFlow::Continue(())
401    })?;
402
403    for (issuer, credential, bytes) in credentials {
404        if !authenticated_issuers.contains(&issuer) {
405            return Err(RegistryMergeError::CredentialOrphan { issuer, credential });
406        }
407        verify_credential_body(&issuer, &credential, &bytes)?;
408
409        if dest.load_credential(&issuer, &credential)?.is_some() {
410            report.credentials_already_present += 1;
411            continue;
412        }
413        dest.store_credential(&issuer, &credential, &bytes)?;
414        report.credentials_imported += 1;
415    }
416    Ok(())
417}
418
419/// Import every source TEL chain for an authenticated issuer (the second leg of
420/// [`merge_credentials_and_tel`]).
421fn merge_tel_chains(
422    source: &dyn RegistryBackend,
423    dest: &dyn RegistryBackend,
424    authenticated_issuers: &HashSet<Prefix>,
425    report: &mut MergedCredentials,
426) -> Result<(), RegistryMergeError> {
427    let mut coordinates: Vec<(Prefix, Said, Said)> = Vec::new();
428    source.visit_tel_registries(&mut |issuer, registry, credential| {
429        coordinates.push((issuer.clone(), registry.clone(), credential.clone()));
430        ControlFlow::Continue(())
431    })?;
432
433    for (issuer, registry, credential) in coordinates {
434        if !authenticated_issuers.contains(&issuer) {
435            return Err(RegistryMergeError::CredentialOrphan { issuer, credential });
436        }
437        merge_one_tel(source, dest, &issuer, &registry, &credential, report)?;
438    }
439    Ok(())
440}
441
442/// Validate and import one TEL coordinate's events, idempotently.
443fn merge_one_tel(
444    source: &dyn RegistryBackend,
445    dest: &dyn RegistryBackend,
446    issuer: &Prefix,
447    registry: &Said,
448    credential: &Said,
449    report: &mut MergedCredentials,
450) -> Result<(), RegistryMergeError> {
451    let refused = |reason: String| RegistryMergeError::TelRefused {
452        issuer: issuer.clone(),
453        registry: registry.clone(),
454        credential: credential.clone(),
455        reason,
456    };
457
458    // Read the source TEL events (raw bytes + each event's own sequence, which is
459    // the storage key — not the iteration position).
460    let mut raw: Vec<(u128, Vec<u8>)> = Vec::new();
461    read_tel_raw(source, issuer, registry, credential, &mut raw).map_err(&refused)?;
462    if raw.is_empty() {
463        return Ok(());
464    }
465
466    // Structural validation BEFORE any write: a byte-flipped TEL event fails its
467    // own SAID recomputation inside validate_tel and is refused whole. An
468    // `iss`/`rev` coordinate carries only that chain's suffix, which validate_tel
469    // rejects as missing its inception — so prepend the registry's `vcp` slot for
470    // the check (the `vcp` coordinate is itself credential == registry).
471    let mut chain: Vec<TelEvent> = Vec::new();
472    if credential != registry {
473        collect_tel(source, issuer, registry, registry, &mut chain).map_err(&refused)?;
474    }
475    collect_tel(source, issuer, registry, credential, &mut chain).map_err(&refused)?;
476    validate_tel(&chain).map_err(|e| refused(e.to_string()))?;
477
478    // Persist verbatim, idempotently (skip an sn the destination already has).
479    let mut present: BTreeSet<u128> = BTreeSet::new();
480    read_tel_sns(dest, issuer, registry, credential, &mut present)
481        .map_err(|reason| refused(format!("local TEL event did not parse: {reason}")))?;
482    for (sn, bytes) in raw {
483        if present.contains(&sn) {
484            report.tel_events_already_present += 1;
485            continue;
486        }
487        dest.append_tel_event(issuer, registry, credential, sn, &bytes)?;
488        report.tel_events_imported += 1;
489    }
490    Ok(())
491}
492
493/// Read one coordinate's TEL events into `(sn, bytes)` pairs, surfacing a parse
494/// failure as a reason string.
495fn read_tel_raw(
496    source: &dyn RegistryBackend,
497    issuer: &Prefix,
498    registry: &Said,
499    credential: &Said,
500    raw: &mut Vec<(u128, Vec<u8>)>,
501) -> Result<(), String> {
502    let mut parse_err: Option<String> = None;
503    source
504        .visit_tel_events(
505            issuer,
506            registry,
507            credential,
508            &mut |bytes| match TelEvent::from_wire_bytes(bytes) {
509                Ok(event) => {
510                    raw.push((tel_event_sn(&event), bytes.to_vec()));
511                    ControlFlow::Continue(())
512                }
513                Err(e) => {
514                    parse_err = Some(format!("TEL event did not parse: {e}"));
515                    ControlFlow::Break(())
516                }
517            },
518        )
519        .map_err(|e| e.to_string())?;
520    match parse_err {
521        Some(reason) => Err(reason),
522        None => Ok(()),
523    }
524}
525
526/// Collect the destination coordinate's already-present TEL sequence numbers.
527fn read_tel_sns(
528    dest: &dyn RegistryBackend,
529    issuer: &Prefix,
530    registry: &Said,
531    credential: &Said,
532    present: &mut BTreeSet<u128>,
533) -> Result<(), String> {
534    let mut parse_err: Option<String> = None;
535    dest.visit_tel_events(
536        issuer,
537        registry,
538        credential,
539        &mut |bytes| match TelEvent::from_wire_bytes(bytes) {
540            Ok(event) => {
541                present.insert(tel_event_sn(&event));
542                ControlFlow::Continue(())
543            }
544            Err(e) => {
545                parse_err = Some(e.to_string());
546                ControlFlow::Break(())
547            }
548        },
549    )
550    .map_err(|e| e.to_string())?;
551    match parse_err {
552        Some(reason) => Err(reason),
553        None => Ok(()),
554    }
555}
556
557/// The storage sequence key for a TEL event (its own `s` field).
558fn tel_event_sn(event: &TelEvent) -> u128 {
559    match event {
560        TelEvent::Vcp(vcp) => vcp.s.value(),
561        TelEvent::Iss(iss) => iss.s.value(),
562        TelEvent::Rev(rev) => rev.s.value(),
563    }
564}
565
566/// Refuse a credential body whose recomputed SAID or issuer disagrees with the
567/// SAID-addressed path it was served under.
568fn verify_credential_body(
569    issuer: &Prefix,
570    credential: &Said,
571    bytes: &[u8],
572) -> Result<(), RegistryMergeError> {
573    let refused = |reason: String| RegistryMergeError::CredentialRefused {
574        issuer: issuer.clone(),
575        credential: credential.clone(),
576        reason,
577    };
578
579    // The stored envelope is `{ "acdc": {…}, "signature": [...] }`; the trust
580    // core here is the ACDC body's content-addressing. The detached issuer
581    // signature is re-checked against the authenticated KEL at verify time.
582    let value: serde_json::Value =
583        serde_json::from_slice(bytes).map_err(|e| refused(format!("blob is not JSON: {e}")))?;
584    let acdc_value = value
585        .get("acdc")
586        .ok_or_else(|| refused("blob has no `acdc` body".to_string()))?;
587    let acdc: Acdc = serde_json::from_value(acdc_value.clone())
588        .map_err(|e| refused(format!("acdc body did not parse: {e}")))?;
589
590    acdc.verify_said()
591        .map_err(|e| refused(format!("acdc SAID does not recompute: {e}")))?;
592    if acdc.d.as_str() != credential.as_str() {
593        return Err(refused(format!(
594            "acdc SAID {} does not match the path SAID {}",
595            acdc.d.as_str(),
596            credential.as_str()
597        )));
598    }
599    if acdc.i.as_str() != issuer.as_str() {
600        return Err(refused(format!(
601            "acdc issuer {} does not match the path issuer {}",
602            acdc.i.as_str(),
603            issuer.as_str()
604        )));
605    }
606    Ok(())
607}
608
609/// Append one coordinate's parsed TEL events onto `chain`, surfacing a parse
610/// failure as the refusal reason string.
611fn collect_tel(
612    source: &dyn RegistryBackend,
613    issuer: &Prefix,
614    registry: &Said,
615    credential: &Said,
616    chain: &mut Vec<TelEvent>,
617) -> Result<(), String> {
618    let mut parse_err: Option<String> = None;
619    source
620        .visit_tel_events(
621            issuer,
622            registry,
623            credential,
624            &mut |bytes| match TelEvent::from_wire_bytes(bytes) {
625                Ok(event) => {
626                    chain.push(event);
627                    ControlFlow::Continue(())
628                }
629                Err(e) => {
630                    parse_err = Some(format!("TEL event did not parse: {e}"));
631                    ControlFlow::Break(())
632                }
633            },
634        )
635        .map_err(|e| e.to_string())?;
636    if let Some(reason) = parse_err {
637        return Err(reason);
638    }
639    Ok(())
640}
641
642/// Resolves a delegated event's anchoring seal from the merge's registries —
643/// source first (a pushed registry carries the delegator's KEL alongside the
644/// delegate's), then the destination (the delegator may already be local).
645struct BackendSealLookup<'a> {
646    backends: [&'a dyn RegistryBackend; 2],
647    caps: KelCaps,
648}
649
650impl DelegatorKelLookup for BackendSealLookup<'_> {
651    fn find_seal(&self, delegator_aid: &Prefix, seal_said: &Said) -> Option<SourceSeal> {
652        for backend in self.backends {
653            let Ok(events) = collect_kel_capped(
654                backend,
655                delegator_aid,
656                self.caps.max_events,
657                self.caps.max_bytes,
658            ) else {
659                continue;
660            };
661            if let Some(seal) =
662                KelSealIndex::from_events(&events).find_seal(delegator_aid, seal_said)
663            {
664                return Some(seal);
665            }
666        }
667        None
668    }
669}