Skip to content

feat: [DSM-148] XNetAdvertHandler for XNetPayloadBuilderImpl - #11602

Open
alin-at-dfinity wants to merge 1 commit into
masterfrom
alin/DSM-148-advert-handler
Open

alin-at-dfinity wants to merge 1 commit into
masterfrom
alin/DSM-148-advert-handler

Conversation

@alin-at-dfinity

Copy link
Copy Markdown
Contributor

Handles an advert — a certified, header-only stream slice pushed by a source subnet — by classifying it against our certified state first (NothingNew, answered with our own certified header) and then against the pool. Only the two outcomes that act on the advert, NothingNew and Actionable, require a threshold-signature verification; the rest are dropped on the unverified claim alone, which can only understate what the peer holds.

Every verified header is recorded in peer_headers: our only record of how far the peer has garbage collected its messages, hence of whether we still owe it an advert.

Nothing calls this yet; the endpoint follows.

@alin-at-dfinity
alin-at-dfinity requested a review from a team as a code owner September 17, 2026 08:51
@github-actions github-actions Bot added the feat label Sep 17, 2026
@zeropath-ai

zeropath-ai Bot commented Sep 17, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to d194f26.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► rs/interfaces/src/messaging.rs
    Add XNetAdvertHandler trait and XNetAdvertError/Error handling
► rs/xnet/payload_builder/src/lib.rs
    Extend XNetSlicePool trait with classify_advert and record_peer_header; implement XNetAdvertHandler for XNetPayloadBuilderImpl; add classify_advert logic and header retrieval
► rs/xnet/payload_builder/src/test_fixtures.rs
    Add test fixtures and helpers for advert-related tests
► rs/xnet/payload_builder/src/tests.rs
    Add tests for advert handling, including actionable, in-payload, and duplicates
Bug Fix (None explicitly present)
Refactor (None explicitly present)

@alin-at-dfinity
alin-at-dfinity added this pull request to stack #11604 September 17, 2026 09:16
@alin-at-dfinity
alin-at-dfinity removed this pull request from stack #11604 September 17, 2026 10:21
@alin-at-dfinity
alin-at-dfinity added this pull request to stack #11607 September 17, 2026 10:47
@alin-at-dfinity
alin-at-dfinity force-pushed the alin/DSM-148-advert-handler branch from 21f7bf3 to 828ebb1 Compare September 17, 2026 11:48
Base automatically changed from alin/DSM-148-classify-advert to master September 17, 2026 14:58
Handles an advert — a certified, header-only stream slice pushed by a source
subnet — by classifying it against our certified state first (`NothingNew`,
answered with our own certified header) and then against the pool (§6). Only
the two outcomes that act on the advert being genuine, `NothingNew` and
`Actionable`, pay for a threshold-signature verification; the rest are dropped
on the unverified claim alone, which can only understate what the peer holds.

Every verified header is recorded in `peer_headers`: our only record of how far
the peer has garbage collected its messages, hence of whether we still owe it
an advert.

Nothing calls this yet; the endpoint follows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@alin-at-dfinity
alin-at-dfinity force-pushed the alin/DSM-148-advert-handler branch from 828ebb1 to d194f26 Compare September 17, 2026 14:58
/// The reason for rejecting a XNet advert.
#[derive(Debug)]
pub enum XNetAdvertError {
/// Could not be decoded, or its certification could not be verified.

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.

How about distinguishing the two reasons in this enum?

Comment on lines +153 to +154
/// Records the header as the peer's high-water-mark header, unless one with
/// a greater certified height is already on record.

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.

Requirement: the header has been verified.

/// a certification at `CERTIFIED_HEIGHT`.
///
/// Unlike `make_certified_stream_slice`, which goes through `FakeStateManager`
/// and its CBOR encoding, this can be handed to `decode_slice_header()`.

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.

amend the comment by noting that this encoding is pbuf

/// and its CBOR encoding, this can be handed to `decode_slice_header()`.
pub(crate) fn make_advert(stream: &Stream) -> CertifiedStreamSlice {
// `REMOTE_SUBNET`'s state, holding the advertised stream to us. Not our own
// state: the canonical traversal skips the loopback stream.

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.

Suggested change
// state: the canonical traversal skips the loopback stream.
// state: an advert always targets a different subnet (and the canonical traversal skips the loopback stream so it could not be created anyway).

store
.expect_decode_certified_stream_slice()
.times(verifications)
.returning(move |_, _, _| Ok(decoded.clone()));

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.

could we also assert that the input is as expected (to match that we decode "an advert carrying advertised's header" as in the doc comment)?

/// An advert offering messages we have not inducted is actionable, and its
/// header is recorded.
#[tokio::test]
async fn handle_advert_actionable() {

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.

the test handle_advert_redundant_copies starts off the same as this one so maybe they could be merged

assert_matches!(
payload_builder.handle_advert(REMOTE_SUBNET, make_advert(&advertised)),
Ok(XNetAdvertOutcome::InPayload)
);

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.

we could also assert here that the (unverified as asserted by the zero in advert_store(&advertised, 0)) header was not recorded in the pool

assert_matches!(
payload_builder.handle_advert(REMOTE_SUBNET, make_advert(&advertised)),
Ok(XNetAdvertOutcome::NothingNew)
);

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.

we could also assert that the (verified) advert was recorded

LOCAL_SUBNET,
StreamConfig {
message_begin: OWN_MESSAGES_BEGIN,
message_end: OWN_MESSAGES_BEGIN,

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.

is message_end: OWN_MESSAGES_BEGIN chosen so that the only thing we have for the sender is signal_end so that the sender can gc messages and advance message_begin: 0? or is it chosen so that the slice has no messages and thus it is always header-only?

let expected_reply = own_header.clone();
store
.expect_encode_certified_stream_slice()
.returning(move |_, _, _, _, _| Ok(own_header.clone()));

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.

here we could assert that no messages are requested in the stream slice

self.slice_pool.record_peer_header(
source_subnet,
slice.header(),
advert.certification.height,

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.

we should also validate the height witness from the certificate:

fn validate_height_witness(

.certified_stream_store
.decode_certified_stream_slice(source_subnet, registry_version, &advert)
.map_err(|err| XNetAdvertError::Invalid(err.to_string()))?;
debug_assert_eq!(slice.header(), &claimed, "Inconsistent slice decoding");

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.

should we maybe turn this into a critical error since this would indicate malicious behavior?


/// Records the header as the peer's high-water-mark header, unless one with
/// a greater certified height is already on record.
fn record_peer_header(

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.

Peer headers are never pruned.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants