1use alloc::collections::BTreeMap;
5use core::{convert::Infallible, fmt};
6
7use hashbrown::{HashMap, HashSet};
8use primitive_types::U256;
9
10use crate::types::block::{
11 address::Address,
12 output::{ChainId, FoundryId, InputsCommitment, NativeTokens, Output, OutputId, TokenId},
13 payload::transaction::{RegularTransactionEssence, TransactionEssence, TransactionId},
14 unlock::Unlocks,
15 Error,
16};
17
18#[derive(Debug)]
20pub enum ConflictError {
21 InvalidConflict(u8),
23}
24
25impl fmt::Display for ConflictError {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self {
28 Self::InvalidConflict(byte) => write!(f, "invalid conflict byte {byte}"),
29 }
30 }
31}
32
33impl From<Infallible> for ConflictError {
34 fn from(err: Infallible) -> Self {
35 match err {}
36 }
37}
38
39#[cfg(feature = "std")]
40impl std::error::Error for ConflictError {}
41
42#[repr(u8)]
44#[derive(Debug, Copy, Clone, Eq, PartialEq, packable::Packable)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46#[packable(unpack_error = ConflictError)]
47#[packable(tag_type = u8, with_error = ConflictError::InvalidConflict)]
48pub enum ConflictReason {
49 None = 0,
51 InputUtxoAlreadySpent = 1,
53 InputUtxoAlreadySpentInThisMilestone = 2,
55 InputUtxoNotFound = 3,
57 CreatedConsumedAmountMismatch = 4,
59 InvalidSignature = 5,
61 TimelockNotExpired = 6,
63 InvalidNativeTokens = 7,
65 StorageDepositReturnUnfulfilled = 8,
67 InvalidUnlock = 9,
69 InputsCommitmentsMismatch = 10,
71 UnverifiedSender = 11,
73 InvalidChainStateTransition = 12,
75 SemanticValidationFailed = 255,
77}
78
79impl fmt::Display for ConflictReason {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 match self {
82 Self::None => write!(f, "The block has no conflict"),
83 Self::InputUtxoAlreadySpent => write!(f, "The referenced UTXO was already spent"),
84 Self::InputUtxoAlreadySpentInThisMilestone => write!(
85 f,
86 "The referenced UTXO was already spent while confirming this milestone"
87 ),
88 Self::InputUtxoNotFound => write!(f, "The referenced UTXO cannot be found"),
89 Self::CreatedConsumedAmountMismatch => {
90 write!(f, "The sum of the inputs and output values does not match")
91 }
92 Self::InvalidSignature => write!(f, "The unlock block signature is invalid"),
93 Self::TimelockNotExpired => write!(f, "The configured timelock is not yet expired"),
94 Self::InvalidNativeTokens => write!(f, "The native tokens are invalid"),
95 Self::StorageDepositReturnUnfulfilled => write!(
96 f,
97 "The return amount in a transaction is not fulfilled by the output side"
98 ),
99 Self::InvalidUnlock => write!(f, "The input unlock is invalid"),
100 Self::InputsCommitmentsMismatch => write!(f, "The inputs commitment is invalid"),
101 Self::UnverifiedSender => write!(
102 f,
103 " The output contains a Sender with an ident (address) which is not unlocked"
104 ),
105 Self::InvalidChainStateTransition => write!(f, "The chain state transition is invalid"),
106 Self::SemanticValidationFailed => write!(f, "The semantic validation failed"),
107 }
108 }
109}
110
111impl TryFrom<u8> for ConflictReason {
112 type Error = ConflictError;
113
114 fn try_from(c: u8) -> Result<Self, Self::Error> {
115 Ok(match c {
116 0 => Self::None,
117 1 => Self::InputUtxoAlreadySpent,
118 2 => Self::InputUtxoAlreadySpentInThisMilestone,
119 3 => Self::InputUtxoNotFound,
120 4 => Self::CreatedConsumedAmountMismatch,
121 5 => Self::InvalidSignature,
122 6 => Self::TimelockNotExpired,
123 7 => Self::InvalidNativeTokens,
124 8 => Self::StorageDepositReturnUnfulfilled,
125 9 => Self::InvalidUnlock,
126 10 => Self::InputsCommitmentsMismatch,
127 11 => Self::UnverifiedSender,
128 12 => Self::InvalidChainStateTransition,
129 255 => Self::SemanticValidationFailed,
130 x => return Err(Self::Error::InvalidConflict(x)),
131 })
132 }
133}
134
135impl Default for ConflictReason {
136 fn default() -> Self {
137 Self::None
138 }
139}
140
141pub struct ValidationContext<'a> {
143 pub essence: &'a RegularTransactionEssence,
145 pub essence_hash: [u8; 32],
147 pub inputs_commitment: InputsCommitment,
149 pub unlocks: &'a Unlocks,
151 pub milestone_timestamp: u32,
153 pub input_amount: u64,
155 pub input_native_tokens: BTreeMap<TokenId, U256>,
157 pub input_chains: HashMap<ChainId, &'a Output>,
159 pub output_amount: u64,
161 pub output_native_tokens: BTreeMap<TokenId, U256>,
163 pub output_chains: HashMap<ChainId, &'a Output>,
165 pub unlocked_addresses: HashSet<Address>,
167 pub storage_deposit_returns: HashMap<Address, u64>,
169 pub simple_deposits: HashMap<Address, u64>,
171}
172
173impl<'a> ValidationContext<'a> {
174 pub fn new(
176 transaction_id: &TransactionId,
177 essence: &'a RegularTransactionEssence,
178 inputs: impl Iterator<Item = (&'a OutputId, &'a Output)> + Clone,
179 unlocks: &'a Unlocks,
180 milestone_timestamp: u32,
181 ) -> Self {
182 Self {
183 essence,
184 unlocks,
185 essence_hash: TransactionEssence::from(essence.clone()).hash(),
186 inputs_commitment: InputsCommitment::new(inputs.clone().map(|(_, output)| output)),
187 milestone_timestamp,
188 input_amount: 0,
189 input_native_tokens: BTreeMap::<TokenId, U256>::new(),
190 input_chains: inputs
191 .filter_map(|(output_id, input)| {
192 input
193 .chain_id()
194 .map(|chain_id| (chain_id.or_from_output_id(output_id), input))
195 })
196 .collect(),
197 output_amount: 0,
198 output_native_tokens: BTreeMap::<TokenId, U256>::new(),
199 output_chains: essence
200 .outputs()
201 .iter()
202 .enumerate()
203 .filter_map(|(index, output)| {
204 output.chain_id().map(|chain_id| {
205 (
206 chain_id.or_from_output_id(&OutputId::new(*transaction_id, index as u16).unwrap()),
207 output,
208 )
209 })
210 })
211 .collect(),
212 unlocked_addresses: HashSet::new(),
213 storage_deposit_returns: HashMap::new(),
214 simple_deposits: HashMap::new(),
215 }
216 }
217}
218
219pub fn semantic_validation(
221 mut context: ValidationContext<'_>,
222 inputs: &[(&OutputId, &Output)],
223 unlocks: &Unlocks,
224) -> Result<ConflictReason, Error> {
225 if context.essence.inputs_commitment() != &context.inputs_commitment {
227 return Ok(ConflictReason::InputsCommitmentsMismatch);
228 }
229
230 for ((output_id, consumed_output), unlock) in inputs.iter().zip(unlocks.iter()) {
232 let (conflict, amount, consumed_native_tokens, unlock_conditions) = match consumed_output {
233 Output::Basic(output) => (
234 output.unlock(output_id, unlock, inputs, &mut context),
235 output.amount(),
236 output.native_tokens(),
237 output.unlock_conditions(),
238 ),
239 Output::Alias(output) => (
240 output.unlock(output_id, unlock, inputs, &mut context),
241 output.amount(),
242 output.native_tokens(),
243 output.unlock_conditions(),
244 ),
245 Output::Foundry(output) => (
246 output.unlock(output_id, unlock, inputs, &mut context),
247 output.amount(),
248 output.native_tokens(),
249 output.unlock_conditions(),
250 ),
251 Output::Nft(output) => (
252 output.unlock(output_id, unlock, inputs, &mut context),
253 output.amount(),
254 output.native_tokens(),
255 output.unlock_conditions(),
256 ),
257 _ => return Err(Error::UnsupportedOutputKind(consumed_output.kind())),
258 };
259
260 if let Err(conflict) = conflict {
261 return Ok(conflict);
262 }
263
264 if unlock_conditions.is_time_locked(context.milestone_timestamp) {
265 return Ok(ConflictReason::TimelockNotExpired);
266 }
267
268 if !unlock_conditions.is_expired(context.milestone_timestamp) {
269 if let Some(storage_deposit_return) = unlock_conditions.storage_deposit_return() {
270 let amount = context
271 .storage_deposit_returns
272 .entry(*storage_deposit_return.return_address())
273 .or_default();
274
275 *amount = amount
276 .checked_add(storage_deposit_return.amount())
277 .ok_or(Error::StorageDepositReturnOverflow)?;
278 }
279 }
280
281 context.input_amount = context
282 .input_amount
283 .checked_add(amount)
284 .ok_or(Error::ConsumedAmountOverflow)?;
285
286 for native_token in consumed_native_tokens.iter() {
287 let native_token_amount = context.input_native_tokens.entry(*native_token.token_id()).or_default();
288
289 *native_token_amount = native_token_amount
290 .checked_add(native_token.amount())
291 .ok_or(Error::ConsumedNativeTokensAmountOverflow)?;
292 }
293 }
294
295 for created_output in context.essence.outputs() {
297 let (amount, created_native_tokens, features) = match created_output {
298 Output::Basic(output) => {
299 if let Some(address) = output.simple_deposit_address() {
300 let amount = context.simple_deposits.entry(*address).or_default();
301
302 *amount = amount
303 .checked_add(output.amount())
304 .ok_or(Error::CreatedAmountOverflow)?;
305 }
306
307 (output.amount(), output.native_tokens(), output.features())
308 }
309 Output::Alias(output) => (output.amount(), output.native_tokens(), output.features()),
310 Output::Foundry(output) => (output.amount(), output.native_tokens(), output.features()),
311 Output::Nft(output) => (output.amount(), output.native_tokens(), output.features()),
312 _ => return Err(Error::UnsupportedOutputKind(created_output.kind())),
313 };
314
315 if let Some(sender) = features.sender() {
316 if !context.unlocked_addresses.contains(sender.address()) {
317 return Ok(ConflictReason::UnverifiedSender);
318 }
319 }
320
321 context.output_amount = context
322 .output_amount
323 .checked_add(amount)
324 .ok_or(Error::CreatedAmountOverflow)?;
325
326 for native_token in created_native_tokens.iter() {
327 let native_token_amount = context
328 .output_native_tokens
329 .entry(*native_token.token_id())
330 .or_default();
331
332 *native_token_amount = native_token_amount
333 .checked_add(native_token.amount())
334 .ok_or(Error::CreatedNativeTokensAmountOverflow)?;
335 }
336 }
337
338 for (return_address, return_amount) in context.storage_deposit_returns.iter() {
340 if let Some(deposit_amount) = context.simple_deposits.get(return_address) {
341 if deposit_amount < return_amount {
342 return Ok(ConflictReason::StorageDepositReturnUnfulfilled);
343 }
344 } else {
345 return Ok(ConflictReason::StorageDepositReturnUnfulfilled);
346 }
347 }
348
349 if context.input_amount != context.output_amount {
351 return Ok(ConflictReason::CreatedConsumedAmountMismatch);
352 }
353
354 let mut native_token_ids = HashSet::new();
355
356 for (token_id, _input_amount) in context.input_native_tokens.iter() {
358 native_token_ids.insert(token_id);
359 }
360
361 for (token_id, output_amount) in context.output_native_tokens.iter() {
363 let input_amount = context.input_native_tokens.get(token_id).copied().unwrap_or_default();
364
365 if output_amount > &input_amount
366 && !context
367 .output_chains
368 .contains_key(&ChainId::from(FoundryId::from(*token_id)))
369 {
370 return Ok(ConflictReason::InvalidNativeTokens);
371 }
372
373 native_token_ids.insert(token_id);
374 }
375
376 if native_token_ids.len() > NativeTokens::COUNT_MAX as usize {
377 return Ok(ConflictReason::InvalidNativeTokens);
378 }
379
380 for (chain_id, current_state) in context.input_chains.iter() {
382 if Output::verify_state_transition(
383 Some(current_state),
384 context.output_chains.get(chain_id).map(core::ops::Deref::deref),
385 &context,
386 )
387 .is_err()
388 {
389 return Ok(ConflictReason::InvalidChainStateTransition);
390 }
391 }
392
393 for (chain_id, next_state) in context.output_chains.iter() {
395 if context.input_chains.get(chain_id).is_none()
396 && Output::verify_state_transition(None, Some(next_state), &context).is_err()
397 {
398 return Ok(ConflictReason::InvalidChainStateTransition);
399 }
400 }
401
402 Ok(ConflictReason::None)
403}