Skip to main content

tenzro_network/
message.rs

1//! Network message types for Tenzro Network
2
3use bytes::Bytes;
4use serde::{Deserialize, Serialize};
5use tenzro_types::{
6    Block, SignedTransaction, Hash,
7    ModelClass, ArtifactCompleteness, ArtifactMetadata, ModelTopology, ExecutionSupport,
8    RuntimeSupport, NodeNetworkProfile, TrustProfile, WorkerRole,
9    HardwareCapabilities,
10};
11
12/// Network message envelope
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct NetworkMessage {
15    /// Message type and payload
16    pub payload: MessagePayload,
17    /// Message ID for deduplication
18    pub message_id: String,
19    /// Timestamp when message was created
20    pub timestamp: i64,
21}
22
23impl NetworkMessage {
24    /// Creates a new network message
25    pub fn new(payload: MessagePayload) -> Self {
26        Self {
27            payload,
28            message_id: uuid::Uuid::new_v4().to_string(),
29            timestamp: chrono::Utc::now().timestamp_millis(),
30        }
31    }
32
33    /// Serializes the message to bytes using bincode.
34    ///
35    /// Wire format: bincode (length-prefixed varint sequences). Bincode is used
36    /// rather than `serde_json` because consensus payloads embed `Block` and
37    /// `Transaction` types that carry `u128` fields (token amounts, gas), and
38    /// `serde_json` does not support `u128` without `arbitrary_precision`.
39    /// Mismatched encoders/decoders previously caused honest peers to silently
40    /// drop each other's votes, stalling consensus.
41    pub fn to_bytes(&self) -> Result<Bytes, bincode::Error> {
42        let buf = bincode::serialize(self)?;
43        Ok(Bytes::from(buf))
44    }
45
46    /// Deserializes a message from bincode bytes.
47    pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::Error> {
48        bincode::deserialize(bytes)
49    }
50
51    /// Returns the message topic
52    pub fn topic(&self) -> &str {
53        self.payload.topic()
54    }
55}
56
57/// Message payload types.
58///
59/// Uses serde's default externally-tagged enum representation
60/// (`{"Variant": payload}` in JSON, `u32` discriminant + payload in bincode).
61/// Adjacently/internally tagged forms (`#[serde(tag = "...", content = "...")]`)
62/// route through `serialize_struct`/`deserialize_identifier`, which bincode 1.x
63/// does not support — receivers reject every gossip message with
64/// "Bincode does not support Deserializer::deserialize_identifier", stalling
65/// consensus. See bincode-org/bincode#272 and #548.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub enum MessagePayload {
68    /// New block announcement
69    Block(Block),
70
71    /// Block request by hash
72    BlockRequest(Hash),
73
74    /// Block response
75    BlockResponse(Option<Block>),
76
77    /// Transaction broadcast
78    Transaction(SignedTransaction),
79
80    /// Transaction request
81    TransactionRequest(Hash),
82
83    /// Transaction response
84    TransactionResponse(Option<SignedTransaction>),
85
86    /// TEE attestation
87    Attestation(AttestationMessage),
88
89    /// Model inference request
90    InferenceRequest(InferenceRequestMessage),
91
92    /// Model inference response
93    InferenceResponse(InferenceResponseMessage),
94
95    /// Model registration
96    ModelRegistration(ModelRegistrationMessage),
97
98    /// Agent announcement — broadcast by nodes that have registered agents
99    /// so all peers can populate their network_agents cache.
100    AgentAnnouncement(AgentAnnouncementMessage),
101
102    /// Provider announcement — broadcast by nodes serving models/TEE so all
103    /// peers can populate their network_providers cache.
104    ProviderAnnouncement(ProviderAnnouncementMessage),
105
106    /// Peer status update
107    Status(StatusMessage),
108
109    /// Ping message
110    Ping,
111
112    /// Pong response
113    Pong,
114
115    /// Custom application message
116    Custom { topic: String, data: Vec<u8> },
117}
118
119impl MessagePayload {
120    /// Returns the topic for this message type
121    pub fn topic(&self) -> &str {
122        match self {
123            Self::Block(_) | Self::BlockRequest(_) | Self::BlockResponse(_) => "tenzro/blocks",
124            Self::Transaction(_) | Self::TransactionRequest(_) | Self::TransactionResponse(_) => {
125                "tenzro/transactions"
126            }
127            Self::Attestation(_) => "tenzro/attestations",
128            Self::InferenceRequest(_) | Self::InferenceResponse(_) => "tenzro/inference",
129            Self::ModelRegistration(_) => "tenzro/models",
130            Self::AgentAnnouncement(_) => "tenzro/agents",
131            Self::ProviderAnnouncement(_) => "tenzro/providers",
132            Self::Status(_) | Self::Ping | Self::Pong => "tenzro/status",
133            Self::Custom { topic, .. } => topic,
134        }
135    }
136}
137
138/// Consensus message types.
139///
140/// Uses serde's default externally-tagged enum representation. See
141/// `MessagePayload` above for the rationale — `#[serde(tag = "...")]` is
142/// internally-tagged and incompatible with bincode 1.x.
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub enum ConsensusMessage {
145    /// Block proposal.
146    ///
147    /// `timeout_certificate` is `Some(_)` only when the leader is recovering
148    /// from a view timeout — it carries the bincode-serialized
149    /// `tenzro_consensus::timeout::TimeoutCertificate` (2f+1 timeout signatures
150    /// from the previous view) so peers can verify the new view was
151    /// legitimately abandoned (Jolteon `safe_to_extend`, DiemBFT v4 §3.5).
152    ///
153    /// `high_qc_view` is the proposer's local highest-Prepare-QC view at the
154    /// moment of proposing (#171, Aptos SyncInfo pattern). Receivers adopt it
155    /// if higher than their own to fast-forward the lagging-replica case.
156    /// Must satisfy `high_qc_view < block.header.view`.
157    Proposal {
158        block: Box<Block>,
159        proposer: String,
160        round: u64,
161        high_qc_view: u64,
162        /// bincode-serialized `tenzro_consensus::timeout::TimeoutCertificate`,
163        /// or `None` for the steady-state happy path.
164        timeout_certificate: Option<Vec<u8>>,
165        /// bincode-serialized
166        /// `tenzro_consensus::timeout::NoEndorsementCertificate`. Carries f+1
167        /// no-endorsement signatures attesting that no Prepare-QC formed at
168        /// the timed-out view (MonadBFT, arXiv:2502.20692). `Some(_)` is
169        /// required when the leader is proposing a fresh block after a TC
170        /// — receivers reject an unaccompanied fresh block. `None` for the
171        /// steady-state happy path AND when the leader is reproposing the
172        /// existing high-tip block (the parent-hash match suffices).
173        no_endorsement_certificate: Option<Vec<u8>>,
174    },
175    /// Vote on a proposal
176    ///
177    /// Carries a hybrid (Ed25519 + ML-DSA-65) signature and the voter's
178    /// composite public key so peers can verify both legs without an
179    /// out-of-band registry lookup. The two opaque blobs are bincode-
180    /// serialized `CompositeSignature` / `CompositePublicKey` from
181    /// `tenzro_crypto::composite`.
182    ///
183    /// `high_qc_view` is the voter's local highest-Prepare-QC view at the
184    /// moment of voting (#171, Aptos SyncInfo). Bound into the vote's signing
185    /// payload — must match the bound on the inner `Vote` or signature
186    /// verification fails.
187    Vote {
188        block_hash: Hash,
189        voter: String,
190        vote_type: VoteType,
191        round: u64,
192        height: u64,
193        high_qc_view: u64,
194        /// bincode-serialized `tenzro_crypto::composite::CompositeSignature`
195        signature: Vec<u8>,
196        /// bincode-serialized `tenzro_crypto::composite::CompositePublicKey`
197        public_key: Vec<u8>,
198        /// Raw 96-byte BLS12-381 G2 signature over the canonical QC payload
199        /// (`TENZRO_QC_BLS:` || vote_format_version || view || height || block_hash
200        /// || vote_type`). Aggregated by `VoteCollector` into the QC's
201        /// `bls_aggregate`.
202        bls_signature: Vec<u8>,
203    },
204    /// Commit message
205    Commit {
206        block_hash: Hash,
207        signatures: Vec<Vec<u8>>,
208    },
209    /// Pacemaker timeout broadcast (DiemBFT v4 §3.5).
210    ///
211    /// Sent on local view-timer expiry. Receivers at a strictly lower view
212    /// adopt `view` after verifying the sender's hybrid signature — the
213    /// signature is the cryptographic gate (DiemBFT v4 §3.5
214    /// `process_remote_timeout`); no numeric jump cap is applied, since
215    /// stuck replicas may legitimately need to sync forward by many
216    /// thousands of views. This is the backward-sync channel that
217    /// prevents two honest replicas from drifting apart under partial
218    /// synchrony.
219    ///
220    /// The two opaque blobs are bincode-serialized `CompositeSignature` /
221    /// `CompositePublicKey` from `tenzro_crypto::composite`, mirroring the
222    /// Vote variant. Format version is pinned by
223    /// `tenzro_consensus::TIMEOUT_MSG_FORMAT_VERSION`.
224    Timeout {
225        format_version: u8,
226        view: u64,
227        /// Highest Prepare-QC view this voter has observed (≤ `view - 1`).
228        /// Aggregated by the receiver into the TC's `max_high_qc_view()` so
229        /// the next leader can compute the Jolteon `safe_to_extend` predicate.
230        high_qc_view: u64,
231        /// The sender's highest finalized block height. Part of the signed
232        /// payload. Receivers behind this height engage block-sync — the heal
233        /// path for single-block finalization skew (one replica finalized via
234        /// a Commit QC the others never received).
235        finalized_height: u64,
236        voter: tenzro_types::primitives::Address,
237        /// bincode-serialized `tenzro_crypto::composite::CompositeSignature`
238        signature: Vec<u8>,
239        /// bincode-serialized `tenzro_crypto::composite::CompositePublicKey`
240        public_key: Vec<u8>,
241    },
242    /// MonadBFT no-endorsement attestation broadcast (arXiv:2502.20692).
243    ///
244    /// Sent on local view-timer expiry alongside the Timeout broadcast.
245    /// Aggregated by the receiver into a `NoEndorsementCertificate` (f+1
246    /// signatures) which the next leader attaches to a fresh block proposal
247    /// after the timed-out view. The f+1 threshold is the smallest set that
248    /// guarantees at least one honest signer — and any honest signer would
249    /// refuse to sign if it had observed a Prepare-QC at the timed-out view,
250    /// so the NEC is unforgeable evidence that no QC formed.
251    ///
252    /// The two opaque blobs mirror the Vote / Timeout variants — bincode-
253    /// serialized `CompositeSignature` / `CompositePublicKey`. Format version
254    /// is pinned by `tenzro_consensus::NO_ENDORSEMENT_MSG_FORMAT_VERSION`.
255    NoEndorsement {
256        format_version: u8,
257        view: u64,
258        voter: tenzro_types::primitives::Address,
259        /// bincode-serialized `tenzro_crypto::composite::CompositeSignature`
260        signature: Vec<u8>,
261        /// bincode-serialized `tenzro_crypto::composite::CompositePublicKey`
262        public_key: Vec<u8>,
263    },
264}
265
266/// Vote types for consensus
267#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
268pub enum VoteType {
269    /// Prevote
270    Prevote,
271    /// Precommit
272    Precommit,
273}
274
275/// TEE attestation message
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct AttestationMessage {
278    /// Provider identifier
279    pub provider_id: String,
280    /// Attestation report (vendor-specific)
281    pub report: Vec<u8>,
282    /// Report signature
283    pub signature: Vec<u8>,
284    /// Timestamp
285    pub timestamp: i64,
286}
287
288/// Inference request message
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct InferenceRequestMessage {
291    /// Request ID
292    pub request_id: String,
293    /// Model identifier
294    pub model_id: String,
295    /// Input data
296    pub input: Vec<u8>,
297    /// Requester address
298    pub requester: String,
299    /// Payment details
300    pub payment: PaymentDetails,
301}
302
303/// Inference response message
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct InferenceResponseMessage {
306    /// Request ID this responds to
307    pub request_id: String,
308    /// Provider identifier
309    pub provider_id: String,
310    /// Output data
311    pub output: Vec<u8>,
312    /// Computation proof (optional)
313    pub proof: Option<Vec<u8>>,
314}
315
316/// Model registration message — broadcast over gossipsub when a provider
317/// starts serving a model to the network.
318#[derive(Debug, Clone, Default, Serialize, Deserialize)]
319pub struct ModelRegistrationMessage {
320    /// Model identifier
321    pub model_id: String,
322    /// Model name
323    pub name: String,
324    /// Model description
325    pub description: String,
326    /// Model modality
327    pub modality: String,
328    /// Model category (e.g. "chat", "completion", "embedding")
329    #[serde(default)]
330    pub category: String,
331    /// Model parameters (e.g. "3B", "0.8B")
332    #[serde(default)]
333    pub parameters: String,
334    /// Model context length
335    #[serde(default)]
336    pub context_length: u32,
337    /// Provider address
338    pub provider: String,
339    /// Provider's libp2p peer ID (for direct routing)
340    #[serde(default)]
341    pub peer_id: String,
342    /// Pricing information
343    pub pricing: PricingInfo,
344    /// Serving schedule (when this model is available)
345    #[serde(default)]
346    pub schedule: Option<ModelSchedule>,
347    /// Visibility: "network" (gossipsub-discoverable) or "local" (this node only)
348    #[serde(default = "default_visibility")]
349    pub visibility: String,
350    /// TTL in seconds — entries expire if not refreshed (default 120s)
351    #[serde(default = "default_ttl")]
352    pub ttl_secs: u64,
353    /// Whether this is a withdrawal (model stopped serving)
354    #[serde(default)]
355    pub withdrawn: bool,
356    /// RPC endpoint for inference requests (e.g. "http://10.128.0.3:8545")
357    #[serde(default)]
358    pub rpc_endpoint: String,
359    /// RFC-0007: High-level model class classification
360    #[serde(default)]
361    pub model_class: ModelClass,
362    /// RFC-0007: Which artifact types are present, determining supported execution modes
363    #[serde(default)]
364    pub artifact_completeness: ArtifactCompleteness,
365    /// RFC-0007: Downloadable artifact descriptors (weights, shards, tokenizers)
366    #[serde(default)]
367    pub artifacts: Vec<ArtifactMetadata>,
368    /// RFC-0007: Internal topology metadata for MoE and large-scale models
369    #[serde(default)]
370    pub topology: ModelTopology,
371    /// RFC-0007: Execution modes this provider can serve for this model
372    #[serde(default)]
373    pub execution_support: ExecutionSupport,
374}
375
376fn default_visibility() -> String {
377    "network".to_string()
378}
379
380fn default_ttl() -> u64 {
381    120
382}
383
384fn default_agent_ttl() -> u64 {
385    180
386}
387
388fn default_provider_ttl() -> u64 {
389    120
390}
391
392/// Agent announcement message — broadcast over gossipsub topic "tenzro/agents"
393/// every 60s by nodes that have registered agents. All peers merge incoming
394/// announcements into their `network_agents` DashMap so any node can discover
395/// every agent in the network without a central registry.
396#[derive(Debug, Clone, Serialize, Deserialize)]
397pub struct AgentAnnouncementMessage {
398    /// Agent identifier
399    pub agent_id: String,
400    /// Human-readable agent name
401    pub name: String,
402    /// Agent type (e.g. "tenzroclaw", "custom")
403    #[serde(default)]
404    pub agent_type: String,
405    /// Capability names this agent exposes
406    #[serde(default)]
407    pub capabilities: Vec<String>,
408    /// Lifecycle status (e.g. "active", "suspended")
409    #[serde(default)]
410    pub status: String,
411    /// libp2p peer ID of the originating node
412    #[serde(default)]
413    pub origin_peer_id: String,
414    /// RPC endpoint of the originating node (for direct routing)
415    #[serde(default)]
416    pub rpc_endpoint: String,
417    /// Unix timestamp (ms) when this announcement was created
418    pub timestamp: i64,
419    /// TTL in seconds — entries expire if not refreshed (default 180s)
420    #[serde(default = "default_agent_ttl")]
421    pub ttl_secs: u64,
422}
423
424/// Provider announcement message — broadcast over gossipsub topic "tenzro/providers"
425/// every 60s by nodes serving models or TEE services. All peers merge incoming
426/// announcements into their `network_providers` DashMap so any node can discover
427/// every provider in the network without a central registry.
428#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct ProviderAnnouncementMessage {
430    /// libp2p peer ID of the announcing node
431    pub peer_id: String,
432    /// Wallet/account address of the provider
433    pub provider_address: String,
434    /// Provider type (e.g. "llm", "tee", "general")
435    #[serde(default)]
436    pub provider_type: String,
437    /// Model IDs currently being served by this node
438    #[serde(default)]
439    pub served_models: Vec<String>,
440    /// Capability labels (e.g. "inference", "tee-attestation")
441    #[serde(default)]
442    pub capabilities: Vec<String>,
443    /// HTTP RPC endpoint for direct inference routing (e.g. "http://10.128.0.5:8545")
444    #[serde(default)]
445    pub rpc_endpoint: String,
446    /// Lifecycle status (e.g. "active", "draining")
447    #[serde(default)]
448    pub status: String,
449    /// Unix timestamp (ms) when this announcement was created
450    pub timestamp: i64,
451    /// TTL in seconds — entries expire if not refreshed (default 120s)
452    #[serde(default = "default_provider_ttl")]
453    pub ttl_secs: u64,
454    /// RFC-0007: Runtime capabilities of this provider node
455    #[serde(default)]
456    pub runtime_support: RuntimeSupport,
457    /// RFC-0007: Network topology profile of this node
458    #[serde(default)]
459    pub network_profile: NodeNetworkProfile,
460    /// RFC-0007: TEE trust provenance for this provider
461    #[serde(default)]
462    pub trust_profile: TrustProfile,
463    /// RFC-0007: Worker roles this node can fulfil in distributed inference
464    #[serde(default)]
465    pub worker_roles: Vec<WorkerRole>,
466    /// Hardware envelope of this provider node — RAM, VRAM, disk, CPU, TEE
467    /// availability. Populated at announcement-build time from the local
468    /// `HardwareCapabilities::detect()` result so consumers can route by
469    /// memory / GPU / TEE class without an extra RPC round-trip.
470    #[serde(default)]
471    pub hardware: HardwareCapabilities,
472    /// Geographic locality declared by the operator (free-form identifier
473    /// such as `us-central1-a`, `eu-west`, `ap-southeast-1`). `None` means
474    /// the provider declined to declare a region; consumers must treat
475    /// `None` as "unknown geography", not as a wildcard match.
476    #[serde(default)]
477    pub geography: Option<String>,
478}
479
480/// Schedule for when a model is available for serving
481#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct ModelSchedule {
483    /// Whether scheduling is enabled (if false, model is always available)
484    pub enabled: bool,
485    /// Start hour (0-23) in the provider's timezone
486    #[serde(default)]
487    pub start_hour: u8,
488    /// End hour (0-23) in the provider's timezone
489    #[serde(default = "default_end_hour")]
490    pub end_hour: u8,
491    /// Timezone (e.g. "UTC", "America/New_York")
492    #[serde(default = "default_timezone")]
493    pub timezone: String,
494    /// Days of the week the model is available (0=Sun, 1=Mon, ..., 6=Sat)
495    #[serde(default = "default_days")]
496    pub days_of_week: Vec<u8>,
497}
498
499fn default_end_hour() -> u8 {
500    23
501}
502
503fn default_timezone() -> String {
504    "UTC".to_string()
505}
506
507fn default_days() -> Vec<u8> {
508    vec![0, 1, 2, 3, 4, 5, 6]
509}
510
511/// Payment details for inference requests
512#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct PaymentDetails {
514    /// Amount in TNZO
515    pub amount: u64,
516    /// Payment transaction hash
517    pub tx_hash: Option<Hash>,
518}
519
520/// Pricing information for models
521#[derive(Debug, Clone, Default, Serialize, Deserialize)]
522pub struct PricingInfo {
523    /// Price per request in TNZO
524    pub per_request: u64,
525    /// Price per token (for LLMs)
526    pub per_token: Option<u64>,
527}
528
529/// Status message for peer synchronization.
530///
531/// Broadcast on `tenzro/status` every 10s by every node and consumed
532/// by `PeerStatusTracker` to compute a network-tip estimate for `eth_syncing`
533/// and to discover TEE-capable peers for confidential-compute routing.
534/// `peer_id` is embedded so subscribers can attribute the message to a sender —
535/// gossipsub does not surface the originating PeerId to topic subscribers,
536/// only to the swarm event handler.
537///
538/// # TEE capability advertisement
539///
540/// Every node advertises its TEE capability here so peers can route
541/// confidential-compute and custodial-key workloads to TEE-equipped nodes
542/// without requiring out-of-band discovery. All nodes participate in
543/// consensus regardless of `tee_capable`; the field is purely a routing
544/// hint for TEE-gated workloads (confidential AI inference, custodial
545/// key management, attestation issuance).
546#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct StatusMessage {
548    /// libp2p PeerId of the sender, base58-encoded. Subscribers parse this
549    /// back into a `PeerId` via `PeerId::from_str`. Required so the
550    /// `PeerStatusTracker` can key entries by peer.
551    pub peer_id: String,
552    /// Current best block hash
553    pub best_block: Hash,
554    /// Current block height
555    pub height: u64,
556    /// Chain ID
557    pub chain_id: u64,
558    /// Protocol version
559    pub protocol_version: String,
560    /// Whether this node has a TEE provider available and can serve
561    /// confidential-compute / custodial workloads on behalf of peers.
562    pub tee_capable: bool,
563    /// TEE vendor for this node, if any (`None` on commodity hardware).
564    /// Peers consult this when selecting a TEE provider for a specific
565    /// vendor requirement (e.g. SEV-SNP-only workloads).
566    pub tee_vendor: Option<tenzro_types::tee::TeeVendor>,
567}
568
569/// Message validation
570pub fn validate_message(msg: &NetworkMessage) -> crate::error::Result<()> {
571    // Check timestamp is not too far in the future (allow 5 minute clock skew)
572    let now = chrono::Utc::now().timestamp_millis();
573    if msg.timestamp > now + 300_000 {
574        return Err(crate::error::NetworkError::InvalidMessage("Message timestamp is too far in the future".to_string()));
575    }
576
577    // Check message is not too old (reject messages older than 1 hour)
578    if now - msg.timestamp > 3_600_000 {
579        return Err(crate::error::NetworkError::InvalidMessage("Message is too old".to_string()));
580    }
581
582    // Additional payload-specific validation
583    match &msg.payload {
584        MessagePayload::Block(block) => {
585            if block.header.height.0 == 0 && block.header.prev_hash != Hash::zero() {
586                return Err(crate::error::NetworkError::InvalidMessage("Genesis block must have zero prev_hash".to_string()));
587            }
588        }
589        MessagePayload::InferenceRequest(req) => {
590            if req.request_id.is_empty() {
591                return Err(crate::error::NetworkError::InvalidMessage("Inference request must have a request ID".to_string()));
592            }
593            if req.model_id.is_empty() {
594                return Err(crate::error::NetworkError::InvalidMessage("Inference request must specify a model ID".to_string()));
595            }
596        }
597        _ => {}
598    }
599
600    Ok(())
601}
602
603#[cfg(test)]
604mod tests {
605    use super::*;
606
607    #[test]
608    fn test_message_serialization() {
609        let msg = NetworkMessage::new(MessagePayload::Ping);
610        let bytes = msg.to_bytes().unwrap();
611        let decoded = NetworkMessage::from_bytes(&bytes).unwrap();
612
613        assert_eq!(msg.message_id, decoded.message_id);
614        assert_eq!(msg.timestamp, decoded.timestamp);
615    }
616
617    #[test]
618    fn test_message_topics() {
619        assert_eq!(MessagePayload::Ping.topic(), "tenzro/status");
620        assert_eq!(
621            MessagePayload::Custom {
622                topic: "test/topic".to_string(),
623                data: vec![]
624            }
625            .topic(),
626            "test/topic"
627        );
628    }
629}