1use core::{convert::Infallible, fmt};
5
6use hashbrown::{HashMap, HashSet};
7use primitive_types::U256;
8
9use crate::{
10 address::Address,
11 error::Error,
12 output::{ChainId, FoundryId, InputsCommitment, NativeTokens, Output, OutputId, TokenId},
13 payload::transaction::{RegularTransactionEssence, TransactionEssence, TransactionId},
14 unlock::Unlocks,
15};
16
17#[derive(Debug)]
19pub enum ConflictError {
20 InvalidConflict(u8),
22}
23
24impl fmt::Display for ConflictError {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 ConflictError::InvalidConflict(byte) => write!(f, "invalid conflict byte {byte}"),
28 }
29 }
30}
31
32impl From<Infallible> for ConflictError {
33 fn from(err: Infallible) -> Self {
34 match err {}
35 }
36}
37
38#[cfg(feature = "std")]
39impl std::error::Error for ConflictError {}
40
41#[repr(u8)]
43#[derive(Debug, Copy, Clone, Eq, PartialEq, packable::Packable)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45#[packable(unpack_error = ConflictError)]
46#[packable(tag_type = u8, with_error = ConflictError::InvalidConflict)]
47pub enum ConflictReason {
48 None = 0,
50 InputUtxoAlreadySpent = 1,
52 InputUtxoAlreadySpentInThisMilestone = 2,
54 InputUtxoNotFound = 3,
56 CreatedConsumedAmountMismatch = 4,
58 InvalidSignature = 5,
60 TimelockNotExpired = 6,
62 InvalidNativeTokens = 7,
64 StorageDepositReturnUnfulfilled = 8,
66 InvalidUnlock = 9,
68 InputsCommitmentsMismatch = 10,
70 UnverifiedSender = 11,
72 InvalidChainStateTransition = 12,
74 SemanticValidationFailed = 255,
76}
77
78impl Default for ConflictReason {
79 fn default() -> Self {
80 Self::None
81 }
82}
83
84impl TryFrom<u8> for ConflictReason {
85 type Error = ConflictError;
86
87 fn try_from(c: u8) -> Result<Self, Self::Error> {
88 Ok(match c {
89 0 => Self::None,
90 1 => Self::InputUtxoAlreadySpent,
91 2 => Self::InputUtxoAlreadySpentInThisMilestone,
92 3 => Self::InputUtxoNotFound,
93 4 => Self::CreatedConsumedAmountMismatch,
94 5 => Self::InvalidSignature,
95 6 => Self::TimelockNotExpired,
96 7 => Self::InvalidNativeTokens,
97 8 => Self::StorageDepositReturnUnfulfilled,
98 9 => Self::InvalidUnlock,
99 10 => Self::InputsCommitmentsMismatch,
100 11 => Self::UnverifiedSender,
101 12 => Self::InvalidChainStateTransition,
102 255 => Self::SemanticValidationFailed,
103 x => return Err(Self::Error::InvalidConflict(x)),
104 })
105 }
106}
107
108pub struct ValidationContext<'a> {
110 pub essence: &'a RegularTransactionEssence,
112 pub essence_hash: [u8; 32],
114 pub inputs_commitment: InputsCommitment,
116 pub unlocks: &'a Unlocks,
118 pub milestone_timestamp: u32,
120 pub input_amount: u64,
122 pub input_native_tokens: HashMap<TokenId, U256>,
124 pub input_chains: HashMap<ChainId, &'a Output>,
126 pub output_amount: u64,
128 pub output_native_tokens: HashMap<TokenId, U256>,
130 pub output_chains: HashMap<ChainId, &'a Output>,
132 pub unlocked_addresses: HashSet<Address>,
134 pub storage_deposit_returns: HashMap<Address, u64>,
136 pub simple_deposits: HashMap<Address, u64>,
138}
139
140impl<'a> ValidationContext<'a> {
141 pub fn new(
143 transaction_id: &TransactionId,
144 essence: &'a RegularTransactionEssence,
145 inputs: impl Iterator<Item = (&'a OutputId, &'a Output)> + Clone,
146 unlocks: &'a Unlocks,
147 milestone_timestamp: u32,
148 ) -> Self {
149 Self {
150 essence,
151 unlocks,
152 essence_hash: TransactionEssence::from(essence.clone()).hash(),
153 inputs_commitment: InputsCommitment::new(inputs.clone().map(|(_, output)| output)),
154 milestone_timestamp,
155 input_amount: 0,
156 input_native_tokens: HashMap::<TokenId, U256>::new(),
157 input_chains: inputs
158 .filter_map(|(output_id, input)| {
159 input
160 .chain_id()
161 .map(|chain_id| (chain_id.or_from_output_id(*output_id), input))
162 })
163 .collect(),
164 output_amount: 0,
165 output_native_tokens: HashMap::<TokenId, U256>::new(),
166 output_chains: essence
167 .outputs()
168 .iter()
169 .enumerate()
170 .filter_map(|(index, output)| {
171 output.chain_id().map(|chain_id| {
172 (
173 chain_id.or_from_output_id(OutputId::new(*transaction_id, index as u16).unwrap()),
174 output,
175 )
176 })
177 })
178 .collect(),
179 unlocked_addresses: HashSet::new(),
180 storage_deposit_returns: HashMap::new(),
181 simple_deposits: HashMap::new(),
182 }
183 }
184}
185
186pub fn semantic_validation(
188 mut context: ValidationContext,
189 inputs: &[(OutputId, &Output)],
190 unlocks: &Unlocks,
191) -> Result<ConflictReason, Error> {
192 if context.essence.inputs_commitment() != &context.inputs_commitment {
194 return Ok(ConflictReason::InputsCommitmentsMismatch);
195 }
196
197 for ((output_id, consumed_output), unlock) in inputs.iter().zip(unlocks.iter()) {
199 let (conflict, amount, consumed_native_tokens, unlock_conditions) = match consumed_output {
200 Output::Basic(output) => (
201 output.unlock(output_id, unlock, inputs, &mut context),
202 output.amount(),
203 output.native_tokens(),
204 output.unlock_conditions(),
205 ),
206 Output::Alias(output) => (
207 output.unlock(output_id, unlock, inputs, &mut context),
208 output.amount(),
209 output.native_tokens(),
210 output.unlock_conditions(),
211 ),
212 Output::Foundry(output) => (
213 output.unlock(output_id, unlock, inputs, &mut context),
214 output.amount(),
215 output.native_tokens(),
216 output.unlock_conditions(),
217 ),
218 Output::Nft(output) => (
219 output.unlock(output_id, unlock, inputs, &mut context),
220 output.amount(),
221 output.native_tokens(),
222 output.unlock_conditions(),
223 ),
224 _ => return Err(Error::UnsupportedOutputKind(consumed_output.kind())),
225 };
226
227 if let Err(conflict) = conflict {
228 return Ok(conflict);
229 }
230
231 if unlock_conditions.is_time_locked(context.milestone_timestamp) {
232 return Ok(ConflictReason::TimelockNotExpired);
233 }
234
235 if !unlock_conditions.is_expired(context.milestone_timestamp) {
236 if let Some(storage_deposit_return) = unlock_conditions.storage_deposit_return() {
237 let amount = context
238 .storage_deposit_returns
239 .entry(*storage_deposit_return.return_address())
240 .or_default();
241
242 *amount = amount
243 .checked_add(storage_deposit_return.amount())
244 .ok_or(Error::StorageDepositReturnOverflow)?;
245 }
246 }
247
248 context.input_amount = context
249 .input_amount
250 .checked_add(amount)
251 .ok_or(Error::ConsumedAmountOverflow)?;
252
253 for native_token in consumed_native_tokens.iter() {
254 let native_token_amount = context.input_native_tokens.entry(*native_token.token_id()).or_default();
255
256 *native_token_amount = native_token_amount
257 .checked_add(native_token.amount())
258 .ok_or(Error::ConsumedNativeTokensAmountOverflow)?;
259 }
260 }
261
262 for created_output in context.essence.outputs() {
264 let (amount, created_native_tokens, features) = match created_output {
265 Output::Basic(output) => {
266 if let Some(address) = output.simple_deposit_address() {
267 let amount = context.simple_deposits.entry(*address).or_default();
268
269 *amount = amount
270 .checked_add(output.amount())
271 .ok_or(Error::CreatedAmountOverflow)?;
272 }
273
274 (output.amount(), output.native_tokens(), output.features())
275 }
276 Output::Alias(output) => (output.amount(), output.native_tokens(), output.features()),
277 Output::Foundry(output) => (output.amount(), output.native_tokens(), output.features()),
278 Output::Nft(output) => (output.amount(), output.native_tokens(), output.features()),
279 _ => return Err(Error::UnsupportedOutputKind(created_output.kind())),
280 };
281
282 if let Some(sender) = features.sender() {
283 if !context.unlocked_addresses.contains(sender.address()) {
284 return Ok(ConflictReason::UnverifiedSender);
285 }
286 }
287
288 context.output_amount = context
289 .output_amount
290 .checked_add(amount)
291 .ok_or(Error::CreatedAmountOverflow)?;
292
293 for native_token in created_native_tokens.iter() {
294 let native_token_amount = context
295 .output_native_tokens
296 .entry(*native_token.token_id())
297 .or_default();
298
299 *native_token_amount = native_token_amount
300 .checked_add(native_token.amount())
301 .ok_or(Error::CreatedNativeTokensAmountOverflow)?;
302 }
303 }
304
305 for (return_address, return_amount) in context.storage_deposit_returns.iter() {
307 if let Some(deposit_amount) = context.simple_deposits.get(return_address) {
308 if deposit_amount < return_amount {
309 return Ok(ConflictReason::StorageDepositReturnUnfulfilled);
310 }
311 } else {
312 return Ok(ConflictReason::StorageDepositReturnUnfulfilled);
313 }
314 }
315
316 if context.input_amount != context.output_amount {
318 return Ok(ConflictReason::CreatedConsumedAmountMismatch);
319 }
320
321 let mut native_token_ids = HashSet::new();
322
323 for (token_id, _input_amount) in context.input_native_tokens.iter() {
325 native_token_ids.insert(token_id);
326 }
327
328 for (token_id, output_amount) in context.output_native_tokens.iter() {
330 let input_amount = context.input_native_tokens.get(token_id).copied().unwrap_or_default();
331
332 if output_amount > &input_amount
333 && !context
334 .output_chains
335 .contains_key(&ChainId::from(FoundryId::from(*token_id)))
336 {
337 return Ok(ConflictReason::InvalidNativeTokens);
338 }
339
340 native_token_ids.insert(token_id);
341 }
342
343 if native_token_ids.len() > NativeTokens::COUNT_MAX as usize {
344 return Ok(ConflictReason::InvalidNativeTokens);
345 }
346
347 for (chain_id, current_state) in context.input_chains.iter() {
349 if Output::verify_state_transition(
350 Some(current_state),
351 context.output_chains.get(chain_id).map(core::ops::Deref::deref),
352 &context,
353 )
354 .is_err()
355 {
356 return Ok(ConflictReason::InvalidChainStateTransition);
357 }
358 }
359
360 for (chain_id, next_state) in context.output_chains.iter() {
362 if context.input_chains.get(chain_id).is_none()
363 && Output::verify_state_transition(None, Some(next_state), &context).is_err()
364 {
365 return Ok(ConflictReason::InvalidChainStateTransition);
366 }
367 }
368
369 Ok(ConflictReason::None)
370}
371
372#[cfg(feature = "inx")]
373mod inx {
374 use super::*;
375
376 impl From<::inx::proto::block_metadata::ConflictReason> for ConflictReason {
377 fn from(value: ::inx::proto::block_metadata::ConflictReason) -> Self {
378 use ::inx::proto::block_metadata::ConflictReason as InxConflictReason;
379 match value {
380 InxConflictReason::None => ConflictReason::None,
381 InxConflictReason::InputAlreadySpent => ConflictReason::InputUtxoAlreadySpent,
382 InxConflictReason::InputAlreadySpentInThisMilestone => {
383 ConflictReason::InputUtxoAlreadySpentInThisMilestone
384 }
385 InxConflictReason::InputNotFound => ConflictReason::InputUtxoNotFound,
386 InxConflictReason::InputOutputSumMismatch => ConflictReason::CreatedConsumedAmountMismatch,
387 InxConflictReason::InvalidSignature => ConflictReason::InvalidSignature,
388 InxConflictReason::TimelockNotExpired => ConflictReason::TimelockNotExpired,
389 InxConflictReason::InvalidNativeTokens => ConflictReason::InvalidNativeTokens,
390 InxConflictReason::ReturnAmountNotFulfilled => ConflictReason::StorageDepositReturnUnfulfilled,
391 InxConflictReason::InvalidInputUnlock => ConflictReason::InvalidUnlock,
392 InxConflictReason::InvalidInputsCommitment => ConflictReason::InputsCommitmentsMismatch,
393 InxConflictReason::InvalidSender => ConflictReason::UnverifiedSender,
394 InxConflictReason::InvalidChainStateTransition => ConflictReason::InvalidChainStateTransition,
395 InxConflictReason::SemanticValidationFailed => ConflictReason::SemanticValidationFailed,
396 }
397 }
398 }
399
400 impl From<ConflictReason> for ::inx::proto::block_metadata::ConflictReason {
401 fn from(value: ConflictReason) -> Self {
402 match value {
403 ConflictReason::None => Self::None,
404 ConflictReason::InputUtxoAlreadySpent => Self::InputAlreadySpent,
405 ConflictReason::InputUtxoAlreadySpentInThisMilestone => Self::InputAlreadySpentInThisMilestone,
406 ConflictReason::InputUtxoNotFound => Self::InputNotFound,
407 ConflictReason::CreatedConsumedAmountMismatch => Self::InputOutputSumMismatch,
408 ConflictReason::InvalidSignature => Self::InvalidSignature,
409 ConflictReason::TimelockNotExpired => Self::TimelockNotExpired,
410 ConflictReason::InvalidNativeTokens => Self::InvalidNativeTokens,
411 ConflictReason::StorageDepositReturnUnfulfilled => Self::ReturnAmountNotFulfilled,
412 ConflictReason::InvalidUnlock => Self::InvalidInputUnlock,
413 ConflictReason::InputsCommitmentsMismatch => Self::InvalidInputsCommitment,
414 ConflictReason::UnverifiedSender => Self::InvalidSender,
415 ConflictReason::InvalidChainStateTransition => Self::InvalidChainStateTransition,
416 ConflictReason::SemanticValidationFailed => Self::SemanticValidationFailed,
417 }
418 }
419 }
420}