1
// Copyright (C) Moondance Labs Ltd.
2
// This file is part of Tanssi.
3

            
4
// Tanssi is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Tanssi is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Tanssi.  If not, see <http://www.gnu.org/licenses/>
16

            
17
extern crate alloc;
18

            
19
use crate::processors::v2::fallback_message_processor::{
20
    AssetTrapFallbackProcessor, SymbioticFallbackProcessor,
21
};
22
use crate::processors::v2::{
23
    CodecError, FallbackMessageProcessor, MessageExtractionError, MessageProcessorWithFallback,
24
};
25
use alloc::boxed::Box;
26
use alloc::format;
27
use alloc::string::ToString;
28
use core::marker::PhantomData;
29
use parity_scale_codec::Decode;
30
use snowbridge_inbound_queue_primitives::v2::{Message, MessageProcessorError, Payload};
31
use sp_core::{Get, H160};
32
use sp_runtime::Weight;
33
use tp_bridge::symbiotic_message_processor::{
34
    InboundCommand, Message as SymbioticMessage, Payload as SymbioticPayload, MAGIC_BYTES,
35
};
36
use v2_processor_proc_macro::MessageProcessor;
37
use xcm::latest::{ExecuteXcm, InteriorLocation, NetworkId};
38
use xcm_executor::traits::WeightBounds;
39

            
40
10
pub fn try_extract_message<T: pallet_external_validators::Config>(
41
10
    message: &Message,
42
10
    gateway_proxy_address: H160,
43
10
) -> Result<SymbioticMessage<T>, MessageExtractionError> {
44
10
    match message.payload {
45
10
        Payload::Raw(ref payload) => {
46
10
            let raw_payload = crate::processors::v2::RawPayload::decode(&mut payload.as_slice())
47
10
                .map_err(|error| MessageExtractionError::InvalidMessage {
48
                    context: "Unable to decode RawMessage".to_string(),
49
                    source: Some(Box::new(CodecError(error))),
50
                })?;
51
10
            match raw_payload {
52
                crate::processors::v2::RawPayload::Xcm(_payload) => {
53
                    Err(MessageExtractionError::UnsupportedMessage {
54
                        context: "Unsupported Message".to_string(),
55
                        source: None,
56
                    })
57
                }
58
10
                crate::processors::v2::RawPayload::Symbiotic(payload) => {
59
10
                    if message.origin != gateway_proxy_address {
60
3
                        return Err(MessageExtractionError::InvalidMessage {
61
3
                            context: format!(
62
3
                                "Symbiotic message origin is {:?} expected {:?}",
63
3
                                message.origin, gateway_proxy_address
64
3
                            ),
65
3
                            source: None,
66
3
                        });
67
7
                    }
68

            
69
7
                    if message.value > 0 || !message.assets.is_empty() {
70
                        return Err(MessageExtractionError::InvalidMessage {
71
                            context: "Symbiotic message cannot have assets".to_string(),
72
                            source: None,
73
                        });
74
7
                    }
75

            
76
7
                    let symbiotic_payload = SymbioticPayload::decode(&mut payload.as_slice())
77
7
                        .map_err(|error| MessageExtractionError::InvalidMessage {
78
3
                            context: "Unable to decode Symbiotic Payload".to_string(),
79
3
                            source: Some(Box::new(CodecError(error))),
80
3
                        })?;
81
4
                    if symbiotic_payload.magic_bytes != MAGIC_BYTES {
82
2
                        return Err(MessageExtractionError::InvalidMessage {
83
2
                            context: format!(
84
2
                                "Symbiotic magic bytes expected: {:?} got: {:?}",
85
2
                                MAGIC_BYTES, symbiotic_payload.magic_bytes
86
2
                            ),
87
2
                            source: None,
88
2
                        });
89
2
                    }
90

            
91
2
                    return Ok(symbiotic_payload.message);
92
                }
93
            }
94
        }
95
        _ => Err(MessageExtractionError::UnsupportedMessage {
96
            context: "Unsupported Message".to_string(),
97
            source: None,
98
        }),
99
    }
100
10
}
101

            
102
1
pub fn process_message<T: pallet_external_validators::Config>(
103
1
    symbiotic_message: SymbioticMessage<T>,
104
1
) -> Result<(), MessageProcessorError> {
105
1
    match symbiotic_message {
106
        tp_bridge::symbiotic_message_processor::Message::V1(
107
            InboundCommand::ReceiveValidators {
108
1
                validators,
109
1
                external_index,
110
            },
111
        ) => {
112
            // It is fine to return an error here as we know that a valid symbiotic message
113
            // does not contain any asset so there is no need to return success here to trap assets.
114
            // Moreover, the failure here might indicate critical issue within runtime, so it is crucial
115
            // that we do not ignore it.
116
1
            pallet_external_validators::Pallet::<T>::set_external_validators_inner(
117
1
                validators,
118
1
                external_index,
119
            )
120
1
            .map_err(|error| MessageProcessorError::ProcessMessage(error))?;
121
1
            Ok(())
122
        }
123
    }
124
1
}
125

            
126
/// Processes Symbiotic protocol messages for external validator set management.
127
///
128
/// This processor handles messages from the Symbiotic middleware that controls validator
129
/// registration and management. Messages must be encoded in `Payload::Raw(RawPayload::Symbiotic)`
130
/// format and originate from the authorized gateway proxy address.
131
///
132
/// # Message Validation
133
///
134
/// Strictly validates incoming messages:
135
/// - **Origin check**: Message must come from the configured gateway proxy address
136
/// - **Asset check**: Symbiotic messages cannot contain assets, ETH value, or execution fees
137
/// - **Magic bytes**: Payload must contain correct set of bytes to identify Symbiotic messages
138
///
139
/// # Supported Commands
140
///
141
/// Currently, processes `InboundCommand::ReceiveValidators` which:
142
/// - Updates the external validator set via `pallet_external_validators`
143
/// - Tracks validator changes by external index
144
/// - Returns errors on validation failures (no asset trapping needed)
145
///
146
/// # Fallback Behavior
147
///
148
/// Uses `SymbioticFallbackProcessor` which conditionally applies asset trapping:
149
/// - **If origin is not gateway**: Assumes user error or malicious behaviour → trap assets via `AssetTrapFallbackProcessor`
150
/// - **If origin is gateway**: Assumes middleware error → return error to signal the problem
151
///
152
/// Unlike `RawMessageProcessor`, this can safely return errors in case origin is gateway since valid Symbiotic messages
153
/// from middleware never contain assets that could be lost. And error in that case indicate critical runtime issues that
154
/// should not be silently ignored.
155
#[derive(MessageProcessor)]
156
pub struct SymbioticMessageProcessor<
157
    T,
158
    GatewayAddress,
159
    DefaultClaimer,
160
    EthereumNetwork,
161
    EthereumUniversalLocation,
162
    TanssiUniversalLocation,
163
    XcmProcessor,
164
    XcmWeigher,
165
    MaxXcmWeight,
166
    SetExternalValidatorsWeight,
167
>(
168
    PhantomData<(
169
        T,
170
        GatewayAddress,
171
        DefaultClaimer,
172
        EthereumNetwork,
173
        EthereumUniversalLocation,
174
        TanssiUniversalLocation,
175
        XcmProcessor,
176
        XcmWeigher,
177
        MaxXcmWeight,
178
        SetExternalValidatorsWeight,
179
    )>,
180
);
181

            
182
impl<
183
        T,
184
        AccountId,
185
        GatewayAddress,
186
        DefaultClaimer,
187
        EthereumNetwork,
188
        EthereumUniversalLocation,
189
        TanssiUniversalLocation,
190
        XcmProcessor,
191
        XcmWeigher,
192
        MaxXcmWeight,
193
        SetExternalValidatorsWeight,
194
    > MessageProcessorWithFallback<AccountId>
195
    for SymbioticMessageProcessor<
196
        T,
197
        GatewayAddress,
198
        DefaultClaimer,
199
        EthereumNetwork,
200
        EthereumUniversalLocation,
201
        TanssiUniversalLocation,
202
        XcmProcessor,
203
        XcmWeigher,
204
        MaxXcmWeight,
205
        SetExternalValidatorsWeight,
206
    >
207
where
208
    T: snowbridge_pallet_inbound_queue::Config
209
        + pallet_xcm::Config
210
        + snowbridge_pallet_system::Config
211
        + pallet_external_validators::Config,
212
    [u8; 32]: From<<T as frame_system::Config>::AccountId>,
213
    GatewayAddress: Get<H160>,
214
    DefaultClaimer: Get<<T as frame_system::Config>::AccountId>,
215
    EthereumNetwork: Get<NetworkId>,
216
    EthereumUniversalLocation: Get<InteriorLocation>,
217
    TanssiUniversalLocation: Get<InteriorLocation>,
218
    XcmProcessor: ExecuteXcm<<T as pallet_xcm::Config>::RuntimeCall>,
219
    XcmWeigher: WeightBounds<<T as pallet_xcm::Config>::RuntimeCall>,
220
    MaxXcmWeight: Get<Weight>,
221
    SetExternalValidatorsWeight: Get<Weight>,
222
{
223
    type Fallback = SymbioticFallbackProcessor<
224
        T,
225
        AssetTrapFallbackProcessor<
226
            T,
227
            GatewayAddress,
228
            DefaultClaimer,
229
            EthereumNetwork,
230
            EthereumUniversalLocation,
231
            TanssiUniversalLocation,
232
            XcmProcessor,
233
            XcmWeigher,
234
            MaxXcmWeight,
235
        >,
236
        GatewayAddress,
237
    >;
238
    type ExtractedMessage = SymbioticMessage<T>;
239

            
240
10
    fn try_extract_message(
241
10
        _sender: &AccountId,
242
10
        message: &Message,
243
10
    ) -> Result<Self::ExtractedMessage, MessageExtractionError> {
244
10
        let gateway_proxy_address = T::GatewayAddress::get();
245
10
        try_extract_message(message, gateway_proxy_address)
246
10
    }
247

            
248
1
    fn process_extracted_message(
249
1
        _sender: AccountId,
250
1
        extracted_message: Self::ExtractedMessage,
251
1
    ) -> Result<Option<Weight>, MessageProcessorError> {
252
1
        process_message(extracted_message).map(|_| Some(SetExternalValidatorsWeight::get()))
253
1
    }
254

            
255
    fn worst_case_message_processor_weight() -> Weight {
256
        // This ensures the worst case covers both the primary and fallback paths regardless of configuration.
257
        MaxXcmWeight::get().max(SetExternalValidatorsWeight::get())
258
    }
259
}