diff --git a/it/docker-compose/clean.sh b/it/docker-compose/clean.sh
new file mode 100755
index 0000000..f7e4efd
--- /dev/null
+++ b/it/docker-compose/clean.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+# Function to delete Docker Compose resources (containers, volumes, and networks)
+clean_the_docker_compose() {
+ echo "Cleaning up Docker Compose resources..."
+ sudo docker compose -f docker-compose-env.yaml -f docker-compose-java.yaml down -v
+ echo "Docker Compose resources cleaned up."
+}
+
+# Function to delete all Docker resources
+delete_all() {
+ echo "Removing all docker resources from the machine..."
+ sudo docker stop $(docker ps -aq) # Stop all running containers
+ sudo docker rm $(docker ps -aq) # Remove all containers
+ sudo docker volume rm $(docker volume ls -q) # Remove all volumes
+ sudo docker network prune -f # Remove all networks
+ echo "All Docker resources deleted."
+}
+
+# Prompt the user for their choice
+echo "Select an option:"
+echo "1) Clean the docker compose"
+echo "2) Remove all docker resources from the machine"
+read -p "Enter your choice (1 or 2): " choice
+
+# Execute the corresponding function based on user input
+case $choice in
+ 1)
+ clean_the_docker_compose
+ ;;
+ 2)
+ delete_all
+ ;;
+ *)
+ echo "Invalid choice. Please enter 1 or 2."
+ ;;
+esac
+
diff --git a/it/src/test/java/it/README.MD b/it/src/test/java/it/README.MD
new file mode 100644
index 0000000..e69de29
diff --git a/pom.xml b/pom.xml
index 75a3c36..d30edb7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,6 +5,7 @@
org.fiware
canis-major
1.0.0
+ pom
io.micronaut
@@ -705,4 +706,8 @@
+
+ canis-major
+ validation-service
+
diff --git a/src/main/java/validator/README.md b/src/main/java/validator/README.md
new file mode 100644
index 0000000..93f3672
--- /dev/null
+++ b/src/main/java/validator/README.md
@@ -0,0 +1,213 @@
+# Canis Major test
+
+Canis Major serves as a blockchain adaptor within the FIWARE ecosystem, providing secure data persistence across blockchain networks and the Context Broker. The workflow is straightforward:
+ - clients submit transactions containing payload data and wallet credentials.
+ - Once validated, the data is stored in an ETSI Broker (e.g., Orion-LD Broker) and simultaneously processed into a Merkle tree structure for blockchain integration.
+ - The system then signs the transaction using the provided wallet credentials and submits it to an Oketh-compatible blockchain.
+
+ ## Integration tests
+ Let's explore the practical implementation through a series of test commands. First, it is needed to execute the integration tests, by running the following commands:
+
+ ```shell
+ cd it
+ docker-compose -f docker-compose/docker-compose-env.yaml -f docker-compose/docker-compose-java.yaml up
+ NGSI_ADDRESS=localhost:4000 mvn clean test
+ ```
+
+## Entity creation
+After running the integartion tests, create an entity by sending this POST request to Canis Major:
+
+```shell
+curl --location 'http://localhost:4000/ngsi-ld/v1/entities/' \
+--header 'Link: ; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"' \
+--header 'Wallet-Type: vault' \
+--header 'Wallet-Token: vault-plaintext-root-token' \
+--header 'Wallet-Address: http://vault:8200/v1/ethereum/accounts/mira' \
+--header 'Content-Type: application/json' \
+--header 'Accept: application/json' \
+--header 'NGSILD-TENANT: orion' \
+--data '{
+ "id": "urn:ngsi-ld:Building:warehouse001",
+ "type": "Building",
+ "category": {
+ "type": "Property",
+ "value": ["warehouse"]
+ },
+ "address": {
+ "type": "Property",
+ "value": {
+ "streetAddress": "Alexanderplatz 2",
+ "addressRegion": "Berlin",
+ "addressLocality": "Mitte",
+ "postalCode": "10178"
+ }
+ }
+}'
+```
+
+### Wallet HTTP Headers
+The headers included in the HTTP request are crucial for interacting with the blockchain. Here's a breakdown of each component and its significance:
+
+#### Wallet-Type
+This header specifies the type of wallet service being used, in this case, "vault". By specifying the wallet type, the system can ensure that it uses the appropriate methods and protocols for that specific wallet service.
+
+#### Wallet-Token
+This header provides an authentication token for accessing the vault service. This token ensures that only authorized users can access the wallet's functionalities, such as retrieving private keys or signing transactions. Without proper authentication, the system would be vulnerable to unauthorized access.
+
+#### Wallet-Address
+This header points to the specific Ethereum account associated with the wallet. The wallet address is a unique identifier for an Ethereum account. It is used to send and receive transactions on the Ethereum blockchain. By specifying the wallet address, the system knows which account to interact with for operations such as sending tokens, signing transactions, or querying account balances.
+
+### Content Headers
+- Content-Type: Declares the request body format as JSON
+- Accept: Indicates the expected response format as JSON
+
+## Retrieve the entity types in the Context broker
+To retrieve the available entity types in the context broker run the following command:
+
+```shell
+curl -L 'http://localhost:1026/ngsi-ld/v1/types' \
+-H 'Link: ; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"' \
+-H 'NGSILD-Tenant: orion'
+```
+The command returns the following response, demonstrating the available entity types in the Context Broker:
+```json
+{
+ "@context":"https://raw.githubusercontent.com/smart-data-models/dataModel.DistributedLedgerTech/master/context.jsonld","id":"urn:ngsi-ld:EntityTypeList:d77ccfa0-b3cd-11ef-ae1b-0242ac120005","type":"EntityTypeList","typeList":["DLTtxReceipt"]
+}
+```
+The @context is using a definition on smart data models for a DLTtxReceipt and the NGSILD-Tenant is defined in the start-up of the tests.
+
+## Retrieve data from Canis Major
+To retrieve detailed receipt information for a specific building entity from the Canis Major, use the following HTTP request:
+
+```shell
+curl -L 'http://localhost:4000/ngsi-ld/v1/entities/urn:ngsi-ld:Building:warehouse001' \
+-H 'Link: ; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"' \
+-H 'Accept: application/json' | jq '.'
+```
+
+The output will be similar to following response:
+
+```json
+{
+ "blockHash": "0x619433204be327dda0d4722482e44310d2f3807e1a23b1fc097ca809f7afb421",
+ "blockNumber": 193,
+ "blockNumberRaw": "0xc1",
+ "cumulativeGasUsed": 23866,
+ "cumulativeGasUsedRaw": "0x5d3a",
+ "from": "0xd9fe663797b75d0b3897d55d35e0b4e72307a63f",
+ "gasUsed": 23866,
+ "gasUsedRaw": "0x5d3a",
+ "logs": [
+ {
+ "address": "0x476059cd57800db8eb88f67c2aa38a6fcf8251e0",
+ "blockHash": "0x619433204be327dda0d4722482e44310d2f3807e1a23b1fc097ca809f7afb421",
+ "blockNumber": 193,
+ "blockNumberRaw": "0xc1",
+ "data": "0x",
+ "logIndex": 0,
+ "logIndexRaw": "0x0",
+ "removed": false,
+ "topics": [
+ "0xa3865c00e01495fc2b86502cae36a4edb139f748682e7d80725a3d6571a482fa",
+ "0xf85c55023889d6ec0b723c8220471171435040e275059f8e222dfa57a50f0dd5",
+ "0x33868c71f7186ea974685b553d8eee61eb1e95e0a2c70ed54fcd13db67920f74"
+ ],
+ "transactionHash": "0x83f05282a30f77dcfe52ca029b10ba80aac5dad5cc46f5dc437b79e860e4dd65",
+ "transactionIndex": 0,
+ "transactionIndexRaw": "0x0",
+ "type": "mined"
+ }
+ ],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000020000000000000008000200000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000004000000000000000000020000000000000000000000000000000000000000000000000010000100000000000000000000000000000000000000000000000000008000000000000000000000420000000000000000000000000000000000000000000000000000000000000000000000000",
+ "status": "0x1",
+ "statusOK": true,
+ "to": "0x476059cd57800db8eb88f67c2aa38a6fcf8251e0",
+ "transactionHash": "0x83f05282a30f77dcfe52ca029b10ba80aac5dad5cc46f5dc437b79e860e4dd65",
+ "transactionIndexRaw": "0x0"
+}
+```
+
+The JSON structure shows a blockchain transaction receipt with the following details:
+#### Transaction Information
+- Block Number: 193 (0xc1)
+- Transaction Status: Successful (statusOK: true)
+- Transaction Hash: 0x83f05282a30f77dcfe52ca029b10ba80aac5dad5cc46f5dc437b79e860e4dd65
+#### Transaction Participants
+- From Address: 0xd9fe663797b75d0b3897d55d35e0b4e72307a63f
+- To Address: 0x476059cd57800db8eb88f67c2aa38a6fcf8251e0
+
+## Retrieve data from the context broker
+To retrieve specific DLT transaction receipts from the the context broker, we'll use an NGSI-LD query that filters entities by type and property values. The query targets entities of type DLTtxReceipt and filters them based on the refEntity property matching the Building entity "urn:ngsi-ld:Building:warehouse001". The attrs=TxReceipts parameter in the NGSI-LD query acts as a data filter to limit the response to only include the TxReceipts property, excluding all other properties of the entity and reducing response payload size.
+
+```shell
+curl -L 'http://localhost:1026/ngsi-ld/v1/entities/?type=DLTtxReceipt&q=refEntity%3D%3D%22urn%3Angsi-ld%3ABuilding%3Awarehouse001%22&attrs=TxReceipts' \
+-H 'Link: ; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"' \
+-H 'NGSILD-Tenant: orion'
+```
+
+The outpot will be similar to the following json response:
+
+```json
+{
+ "@context": "https://raw.githubusercontent.com/smart-data-models/dataModel.DistributedLedgerTech/master/context.jsonld",
+ "id": "urn:ngsi-ld:dlttxreceipt:0xa46d2e3b190d36fbb8af5d0a1c212d8036cf007e6ec4d1309904e052d25e5499",
+ "type": "DLTtxReceipt",
+ "TxReceipts": {
+ "type": "Property",
+ "value": {
+ "blockHash": "0x2022f801a1c094bd3a893ac6f3087c17bb9c673a6b5663f353eded030e1e7161",
+ "blockNumber": 192,
+ "blockNumberRaw": "0xc0",
+ "cumulativeGasUsed": 23866,
+ "cumulativeGasUsedRaw": "0x5d3a",
+ "from": "0x34e5b3f990e55d0651b35c817bafb89d2877cb95",
+ "gasUsed": 23866,
+ "gasUsedRaw": "0x5d3a",
+ "logs": [
+ {
+ "address": "0x476059cd57800db8eb88f67c2aa38a6fcf8251e0",
+ "blockHash": "0x2022f801a1c094bd3a893ac6f3087c17bb9c673a6b5663f353eded030e1e7161",
+ "blockNumber": 192,
+ "blockNumberRaw": "0xc0",
+ "data": "0x",
+ "logIndex": 0,
+ "logIndexRaw": "0x0",
+ "removed": false,
+ "topics": [
+ "0xa3865c00e01495fc2b86502cae36a4edb139f748682e7d80725a3d6571a482fa",
+ "0xf85c55023889d6ec0b723c8220471171435040e275059f8e222dfa57a50f0dd5",
+ "0xefc7a4d4c2393e9b62ac4b93b7d199c71e0bb103fdb00fc1b37d7949ad886ddd"
+ ],
+ "transactionHash": "0xa46d2e3b190d36fbb8af5d0a1c212d8036cf007e6ec4d1309904e052d25e5499",
+ "transactionIndex": 0,
+ "transactionIndexRaw": "0x0",
+ "type": "mined"
+ }
+ ],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000020000000000000008000200000000000000000000000000000000000000000000000100000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000008000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000200000000000000000000000000000000",
+ "status": "0x1",
+ "statusOK": true,
+ "to": "0x476059cd57800db8eb88f67c2aa38a6fcf8251e0",
+ "transactionHash": "0xa46d2e3b190d36fbb8af5d0a1c212d8036cf007e6ec4d1309904e052d25e5499",
+ "transactionIndex": 0,
+ "transactionIndexRaw": "0x0"
+ }
+ }
+}
+```
+
+This JSON response shows a DLTtxReceipt (Distributed Ledger Technology Transaction Receipt) entity in NGSI-LD format:
+
+### Transaction Details
+
+- Entity ID: `urn:ngsi-ld:dlttxreceipt:0xa46d2e3b190d36fbb8af5d0a1c212d8036cf007e6ec4d1309904e052d25e5499`
+- Block Number: 192 (0xc0)
+- Transaction Status: Successful (statusOK: true)
+- Block Hash: `0x2022f801a1c094bd3a893ac6f3087c17bb9c673a6b5663f353eded030e1e7161`
+- Transaction Hash: `0xa46d2e3b190d36fbb8af5d0a1c212d8036cf007e6ec4d1309904e052d25e5499`
+
+### Transaction Participants
+- From Address: `0x34e5b3f990e55d0651b35c817bafb89d2877cb95`
+- To Address: `0x476059cd57800db8eb88f67c2aa38a6fcf8251e0`
+
diff --git a/validation-service/README.md b/validation-service/README.md
new file mode 100644
index 0000000..14c1dbf
--- /dev/null
+++ b/validation-service/README.md
@@ -0,0 +1,45 @@
+# Validation service
+
+## Overview
+The Validation Service is a microservice that verifies transaction data integrity between Orion-LD Context Broker and Canis Major DLT Adapter. It provides a REST API for validating that blockchain transaction data maintains its integrity across both systems, performing security checks to ensure the authenticity and consistency of transaction records and that the transaction information has not been tampered with or corrupted when stored in the DLT.
+
+## Validation Process
+The validation service architecture
+
+
+
+The diagram shows the flow of transaction validation between the Client Domain and Canis Major Domain:
+
+- The process begins when a client initiates validation by sending a request with an Entity ID to the Canis Major Validator
+
+- The validator retrieves transaction details from both Canis Major and the Context Broker
+
+- The validator performs integrity checks on the transaction data
+
+- The validation results are returned to the client
+
+## Validation Checks
+The Canis Major Validator performs a comprehensive field-by-field comparison of transaction data obtained from both the Context Broker and Canis Major.
+
+The service executes the following data integrity verifications:
+
+1. **Status Check**
+ - Verifies if the transaction status matches between both systems.
+
+2. **Block Information**
+ - Confirms block number consistency.
+ - Validates block hash matches.
+
+3. **Transaction Details**
+ - Ensures transaction hash is identical. The hash functions as a digital fingerprint that uniquely identifies a transaction. Any alteration to the transaction data, no matter how small, would result in a completely different hash.
+ - Verifies sender ("from") address matches.
+ - Confirms recipient ("to") address is consistent.
+
+> [!NOTE]
+>
+> Each validation failure generates a specific discrepancy record containing:
+> - The field name where the mismatch occurred
+> - The value reported by Orion-LD
+> - The value reported by Canis Major
+
+
diff --git a/validation-service/Validation_process.png b/validation-service/Validation_process.png
new file mode 100644
index 0000000..2208685
Binary files /dev/null and b/validation-service/Validation_process.png differ
diff --git a/validation-service/pom.xml b/validation-service/pom.xml
new file mode 100644
index 0000000..91a4001
--- /dev/null
+++ b/validation-service/pom.xml
@@ -0,0 +1,147 @@
+
+
+ 4.0.0
+
+
+ org.fiware
+ canis-major
+ 1.0.0
+
+
+ validation-service
+
+
+
+
+ 3.2.7
+ 5.1.0
+ 2.2.1
+
+
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+
+
+
+
+
+ com.github.wistefan
+ ngsi-ld-java-mapping
+ main-SNAPSHOT
+
+
+
+
+ io.micronaut
+ micronaut-validation
+ compile
+
+
+ io.micronaut
+ micronaut-http-server-netty
+ compile
+
+
+ io.micronaut
+ micronaut-http-client
+
+
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
+ io.swagger
+ swagger-annotations
+ 1.6.5
+
+
+ org.openapitools
+ jackson-databind-nullable
+ 0.2.0
+
+
+
+
+
+
+
+ org.openapitools
+ openapi-generator-maven-plugin
+ 6.2.1
+
+
+ canis-major-api
+ none
+
+
+ openapi-ngsi-json
+ none
+
+
+
+ generate.ngsi-ld-client
+
+ generate
+
+
+ ${project.basedir}/src/main/resources/ngsi-ld-api.yaml
+ java
+ native
+ org.fiware.validation_service.client.api
+ org.fiware.validation_service.client.model
+ org.fiware.validation_service.client.invoker
+ false
+ false
+ true
+
+ src/gen/java/main
+ true
+ true
+
+
+
+
+ generate-validation-api
+
+ generate
+
+
+ ${project.basedir}/src/main/resources/validation-api.yaml
+ java
+ native
+ org.fiware.validation_service.api
+ org.fiware.validation_service.model
+ org.fiware.validation_service.invoker
+ false
+ false
+ true
+
+ true
+ src/gen/java/main
+ java8
+ true
+
+
+
+
+
+
+
+
diff --git a/validation-service/src/main/java/org/fiware/validation_service/client/ValidationHttpClient.java b/validation-service/src/main/java/org/fiware/validation_service/client/ValidationHttpClient.java
new file mode 100644
index 0000000..9412b18
--- /dev/null
+++ b/validation-service/src/main/java/org/fiware/validation_service/client/ValidationHttpClient.java
@@ -0,0 +1,62 @@
+package org.fiware.validation_service.client;
+
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+import org.fiware.validation_service.client.api.EntitiesApi;
+import org.fiware.validation_service.client.invoker.ApiClient;
+import org.fiware.validation_service.client.invoker.ApiException;
+import org.fiware.validation_service.client.model.Entity;
+import org.fiware.validation_service.config.NgsiConfig;
+
+import java.util.concurrent.CompletableFuture;
+
+@Singleton
+public class ValidationHttpClient {
+
+ private final EntitiesApi orionLdClient;
+ private final EntitiesApi canisMajorClient;
+ private final String contextUrl;
+ private final String tenant;
+
+ @Inject
+ public ValidationHttpClient(NgsiConfig ngsiConfig) {
+ // Initialize Orion-LD client
+ ApiClient orionApiClient = new ApiClient();
+ orionApiClient.setBasePath("http://" + ngsiConfig.getContextBrokerUrl());
+ this.orionLdClient = new EntitiesApi(orionApiClient);
+
+ // Initialize Canis Major client
+ ApiClient canisMajorApiClient = new ApiClient();
+ canisMajorApiClient.setBasePath("http://" + ngsiConfig.getCanisMajorUrl());
+ this.canisMajorClient = new EntitiesApi(canisMajorApiClient);
+
+ this.contextUrl = ngsiConfig.getContextUrl();
+ this.tenant = ngsiConfig.getNgsildTenant();
+ }
+
+ // Method to fetch an entity by its ID from Orion-LD
+ public CompletableFuture fetchEntityById(String entityId) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ String linkHeader = "<" + contextUrl + ">; rel=\"http://www.w3.org/ns/json-ld#context\"; type=\"application/ld+json\"";
+ return (Entity) orionLdClient.retrieveEntityById(entityId, null, linkHeader, tenant);
+ } catch (ApiException e) {
+ throw new RuntimeException("Failed to fetch entity: " + e.getMessage(), e);
+ }
+ });
+ }
+
+ // Method to fetch data from Canis Major
+ public CompletableFuture fetchCanisMajorData(String entityId) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ String linkHeader = "<" + contextUrl + ">; rel=\"http://www.w3.org/ns/json-ld#context\"; type=\"application/ld+json\"";
+ return (Entity) canisMajorClient.retrieveEntityById(entityId, null, linkHeader, tenant);
+ } catch (ApiException e) {
+ throw new RuntimeException("Failed to fetch data from Canis Major: " + e.getMessage(), e);
+ }
+ });
+ }
+
+
+}
diff --git a/validation-service/src/main/java/org/fiware/validation_service/client/config/NgsiConfig.java b/validation-service/src/main/java/org/fiware/validation_service/client/config/NgsiConfig.java
new file mode 100644
index 0000000..e26597b
--- /dev/null
+++ b/validation-service/src/main/java/org/fiware/validation_service/client/config/NgsiConfig.java
@@ -0,0 +1,30 @@
+package org.fiware.validation_service.config;
+
+import io.micronaut.context.annotation.ConfigurationProperties;
+import lombok.Data;
+
+@Data // Lombok annotation to generate getters and setters
+@ConfigurationProperties("general") // Binds to the 'general' prefix in application.yml
+public class NgsiConfig {
+ private String contextBrokerUrl; // Field for the context broker URL
+ private String canisMajorUrl; // Field for the Canis Major URL
+ private String ngsildTenant; // Field for the tenant configuration
+ private String contextUrl; // Field for the context URL
+
+ // Explicit getters to ensure they're available
+ public String getContextBrokerUrl() {
+ return contextBrokerUrl;
+ }
+
+ public String getCanisMajorUrl() {
+ return canisMajorUrl;
+ }
+
+ public String getNgsildTenant() {
+ return ngsildTenant;
+ }
+
+ public String getContextUrl() {
+ return contextUrl;
+ }
+}
diff --git a/validation-service/src/main/java/org/fiware/validation_service/controller/ValidationController.java b/validation-service/src/main/java/org/fiware/validation_service/controller/ValidationController.java
new file mode 100644
index 0000000..b772f74
--- /dev/null
+++ b/validation-service/src/main/java/org/fiware/validation_service/controller/ValidationController.java
@@ -0,0 +1,35 @@
+package org.fiware.validation_service.controller;
+
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.HttpStatus;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Post;
+import jakarta.inject.Inject;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.fiware.validation_service.model.ValidationResult;
+import org.fiware.validation_service.service.ValidationService;
+
+import java.util.NoSuchElementException;
+
+@Slf4j
+@Controller("/validation")
+@RequiredArgsConstructor
+public class ValidationController {
+
+ private final ValidationService validationService;
+
+ @Post("/transaction/{entityId}")
+ public HttpResponse validateTransaction(String entityId) {
+ try {
+ ValidationResult result = validationService.validateTransaction(entityId);
+ return HttpResponse.ok(result);
+ } catch (NoSuchElementException e) {
+ log.error("Transaction not found: {}", e.getMessage());
+ return HttpResponse.status(HttpStatus.NOT_FOUND);
+ } catch (Exception e) {
+ log.error("Error validating transaction: {}", e.getMessage());
+ return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+}
diff --git a/validation-service/src/main/java/org/fiware/validation_service/service/ValidationService.java b/validation-service/src/main/java/org/fiware/validation_service/service/ValidationService.java
new file mode 100644
index 0000000..e73d4bd
--- /dev/null
+++ b/validation-service/src/main/java/org/fiware/validation_service/service/ValidationService.java
@@ -0,0 +1,118 @@
+package org.fiware.validation_service.service;
+
+import jakarta.inject.Singleton;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.fiware.validation_service.client.ValidationHttpClient;
+import org.fiware.validation_service.client.model.Entity;
+import org.fiware.validation_service.model.Discrepancy;
+import org.fiware.validation_service.model.ValidationResult;
+
+import java.time.OffsetDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+@Slf4j
+@Singleton
+@RequiredArgsConstructor
+public class ValidationService {
+
+ private final ValidationHttpClient validationHttpClient;
+
+ public ValidationResult validateTransaction(String entityId) {
+ try {
+ // Fetch data from both sources asynchronously
+ CompletableFuture orionEntityFuture = validationHttpClient.fetchEntityById(entityId);
+ CompletableFuture canisMajorEntityFuture = validationHttpClient.fetchCanisMajorData(entityId);
+
+ // Wait for both futures to complete
+ CompletableFuture.allOf(orionEntityFuture, canisMajorEntityFuture).join();
+
+ // Get the results
+ Entity orionEntity = orionEntityFuture.get();
+ Entity canisMajorEntity = canisMajorEntityFuture.get();
+
+ // Compare the entities and create a validation result
+ List discrepancies = compareEntities(orionEntity, canisMajorEntity);
+
+ // Create and return the validation result
+ ValidationResult result = new ValidationResult();
+ result.setValid(discrepancies.isEmpty());
+ result.setEntityId(entityId);
+ result.setTimestamp(OffsetDateTime.now());
+ result.setDiscrepancies(discrepancies);
+
+ return result;
+ } catch (InterruptedException | ExecutionException e) {
+ log.error("Error validating transaction: {}", entityId, e);
+ throw new RuntimeException("Failed to validate transaction", e);
+ }
+ }
+
+ private List compareEntities(Entity orionEntity, Entity canisMajorEntity) {
+ List discrepancies = new ArrayList<>();
+
+ // Compare entity type
+ if (!orionEntity.getType().equals(canisMajorEntity.getType())) {
+ Discrepancy discrepancy = new Discrepancy();
+ discrepancy.setField("type");
+ discrepancy.setOrionValue(orionEntity.getType());
+ discrepancy.setCanisMajorValue(canisMajorEntity.getType());
+ discrepancies.add(discrepancy);
+ }
+
+ // Extract TxReceipts from both entities
+ Map orionProperties = (Map) orionEntity.getAdditionalProperties().get("TxReceipts");
+ Map canisMajorProperties = (Map) canisMajorEntity.getAdditionalProperties().get("TxReceipts");
+
+ if (orionProperties != null && canisMajorProperties != null) {
+ Map orionValue = (Map) orionProperties.get("value");
+ Map canisMajorValue = (Map) canisMajorProperties.get("value");
+
+ // Compare key transaction fields
+ compareField(discrepancies, "status", orionValue, canisMajorValue);
+ compareField(discrepancies, "blockNumber", orionValue, canisMajorValue);
+ compareField(discrepancies, "transactionHash", orionValue, canisMajorValue);
+ compareField(discrepancies, "blockHash", orionValue, canisMajorValue);
+ compareField(discrepancies, "from", orionValue, canisMajorValue);
+ compareField(discrepancies, "to", orionValue, canisMajorValue);
+ } else {
+ // If TxReceipts is missing in either entity
+ Discrepancy discrepancy = new Discrepancy();
+ discrepancy.setField("TxReceipts");
+ discrepancy.setOrionValue(orionProperties != null ? "present" : "missing");
+ discrepancy.setCanisMajorValue(canisMajorProperties != null ? "present" : "missing");
+ discrepancies.add(discrepancy);
+ }
+
+ return discrepancies;
+ }
+
+ private void compareField(List discrepancies, String fieldName,
+ Map orionValue, Map canisMajorValue) {
+ Object orionField = orionValue.get(fieldName);
+ Object canisMajorField = canisMajorValue.get(fieldName);
+
+ // Check if field exists in both
+ if (orionField == null || canisMajorField == null) {
+ Discrepancy discrepancy = new Discrepancy();
+ discrepancy.setField(fieldName);
+ discrepancy.setOrionValue(orionField != null ? orionField.toString() : "missing");
+ discrepancy.setCanisMajorValue(canisMajorField != null ? canisMajorField.toString() : "missing");
+ discrepancies.add(discrepancy);
+ return;
+ }
+
+ // Check if values are equal
+ if (!orionField.equals(canisMajorField)) {
+ Discrepancy discrepancy = new Discrepancy();
+ discrepancy.setField(fieldName);
+ discrepancy.setOrionValue(orionField.toString());
+ discrepancy.setCanisMajorValue(canisMajorField.toString());
+ discrepancies.add(discrepancy);
+ }
+ }
+}
diff --git a/validation-service/src/main/resources/application.yml b/validation-service/src/main/resources/application.yml
new file mode 100644
index 0000000..acef394
--- /dev/null
+++ b/validation-service/src/main/resources/application.yml
@@ -0,0 +1,6 @@
+general:
+ contextBrokerUrl: "127.0.0.1:1026" # Replace with your context broker URL
+ canisMajorUrl: "127.0.0.1:4000" # Replace with your Canis Major URL
+ NGSILD_TENANT: "orion" # Replace with your tenant ID
+ contextUrl: "https://raw.githubusercontent.com/smart-data-models/dataModel.DistributedLedgerTech/master/context.jsonld" # NGSI-LD context URL
+
diff --git a/validation-service/src/main/resources/ngsi-ld-api.yaml b/validation-service/src/main/resources/ngsi-ld-api.yaml
new file mode 100644
index 0000000..40294ca
--- /dev/null
+++ b/validation-service/src/main/resources/ngsi-ld-api.yaml
@@ -0,0 +1,234 @@
+openapi: 3.0.0
+info:
+ title: NGSI-LD API
+ version: 1.0.0
+ description: API for interacting with NGSI-LD Context Brokers and Canis Major
+servers:
+ - url: 'http://{serverUrl}'
+ description: NGSI-LD Context Broker
+ variables:
+ serverUrl:
+ default: "127.0.0.1:1026"
+ description: Context Broker URL
+ - url: 'http://{serverUrl}'
+ description: Canis Major DLT Adapter
+ variables:
+ serverUrl:
+ default: "127.0.0.1:4000"
+ description: Canis Major URL
+
+paths:
+ /ngsi-ld/v1/entities:
+ get:
+ summary: Query entities
+ description: Retrieve a set of entities which matches a specific query from an NGSI-LD system
+ operationId: queryEntities
+ tags:
+ - Context Information
+ - Entities
+ parameters:
+ - $ref: '#/components/parameters/type'
+ - $ref: '#/components/parameters/attrs'
+ - $ref: '#/components/parameters/q'
+ - $ref: '#/components/parameters/LinkHeader'
+ - $ref: '#/components/parameters/TenantHeader'
+ responses:
+ '200':
+ description: OK
+ content:
+ application/ld+json:
+ schema:
+ $ref: '#/components/schemas/EntityList'
+ '400':
+ description: Bad request
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/ProblemDetails'
+
+ /ngsi-ld/v1/entities/{entityId}:
+ get:
+ summary: Retrieve entity by ID
+ description: Retrieve an entity by its ID
+ operationId: retrieveEntityById
+ tags:
+ - Context Information
+ - Entities
+ parameters:
+ - name: entityId
+ in: path
+ required: true
+ description: Entity ID
+ schema:
+ type: string
+ - $ref: '#/components/parameters/attrs'
+ - $ref: '#/components/parameters/LinkHeader'
+ - $ref: '#/components/parameters/TenantHeader'
+ responses:
+ '200':
+ description: OK
+ content:
+ application/ld+json:
+ schema:
+ $ref: '#/components/schemas/Entity'
+ '404':
+ description: Entity not found
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/ProblemDetails'
+
+components:
+ parameters:
+ type:
+ name: type
+ in: query
+ description: Entity type
+ schema:
+ type: string
+ attrs:
+ name: attrs
+ in: query
+ description: Comma-separated list of attribute names to be retrieved
+ schema:
+ type: string
+ q:
+ name: q
+ in: query
+ description: Query string for filtering entities
+ schema:
+ type: string
+ LinkHeader:
+ name: Link
+ in: header
+ description: NGSI-LD @context
+ schema:
+ type: string
+ TenantHeader:
+ name: NGSILD-Tenant
+ in: header
+ description: NGSI-LD tenant
+ schema:
+ type: string
+ default: "orion"
+
+ schemas:
+ Entity:
+ type: object
+ properties:
+ id:
+ type: string
+ description: Entity identifier
+ type:
+ type: string
+ description: Entity type
+ additionalProperties: true
+
+ EntityList:
+ type: array
+ items:
+ $ref: '#/components/schemas/Entity'
+
+ ProblemDetails:
+ type: object
+ properties:
+ type:
+ type: string
+ title:
+ type: string
+ status:
+ type: integer
+ detail:
+ type: string
+ instance:
+ type: string
+
+ DLTtxReceipt:
+ type: object
+ properties:
+ id:
+ type: string
+ description: Entity identifier
+ type:
+ type: string
+ description: Entity type (DLTtxReceipt)
+ TxReceipts:
+ type: object
+ properties:
+ type:
+ type: string
+ enum: [Property]
+ value:
+ type: object
+ properties:
+ blockHash:
+ type: string
+ blockNumber:
+ type: integer
+ blockNumberRaw:
+ type: string
+ cumulativeGasUsed:
+ type: integer
+ cumulativeGasUsedRaw:
+ type: string
+ from:
+ type: string
+ gasUsed:
+ type: integer
+ gasUsedRaw:
+ type: string
+ logs:
+ type: array
+ items:
+ type: object
+ properties:
+ address:
+ type: string
+ blockHash:
+ type: string
+ blockNumber:
+ type: integer
+ blockNumberRaw:
+ type: string
+ data:
+ type: string
+ logIndex:
+ type: integer
+ logIndexRaw:
+ type: string
+ removed:
+ type: boolean
+ topics:
+ type: array
+ items:
+ type: string
+ transactionHash:
+ type: string
+ transactionIndex:
+ type: integer
+ transactionIndexRaw:
+ type: string
+ type:
+ type: string
+ logsBloom:
+ type: string
+ status:
+ type: string
+ statusOK:
+ type: boolean
+ to:
+ type: string
+ transactionHash:
+ type: string
+ transactionIndex:
+ type: integer
+ transactionIndexRaw:
+ type: string
+ refEntity:
+ type: object
+ properties:
+ type:
+ type: string
+ enum: [Relationship]
+ object:
+ type: string
diff --git a/validation-service/src/main/resources/validation-api.yaml b/validation-service/src/main/resources/validation-api.yaml
new file mode 100644
index 0000000..819d8c4
--- /dev/null
+++ b/validation-service/src/main/resources/validation-api.yaml
@@ -0,0 +1,64 @@
+openapi: 3.0.0
+info:
+ title: Transaction Validation Service
+ version: 1.0.0
+ description: API for validating transaction data consistency between Orion-LD and Canis Major
+ contact:
+ email: asma.taamallah@fiware.org
+paths:
+ /validation/transaction/{entityId}:
+ post:
+ summary: Validate transaction consistency
+ operationId: validateTransaction
+ tags:
+ - Validation
+ parameters:
+ - name: entityId
+ in: path
+ required: true
+ description: The ID of the transaction entity to validate
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Validation result
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationResult'
+ '404':
+ description: Transaction not found in one or both systems
+ '500':
+ description: Internal server error
+components:
+ schemas:
+ ValidationResult:
+ type: object
+ properties:
+ valid:
+ type: boolean
+ description: Whether the transaction data is consistent between systems
+ entityId:
+ type: string
+ description: The ID of the validated transaction entity
+ timestamp:
+ type: string
+ format: date-time
+ description: When the validation was performed
+ discrepancies:
+ type: array
+ items:
+ $ref: '#/components/schemas/Discrepancy'
+ description: List of discrepancies found (empty if valid is true)
+ Discrepancy:
+ type: object
+ properties:
+ field:
+ type: string
+ description: The field that has a discrepancy
+ orionValue:
+ type: string
+ description: The value from Orion-LD
+ canisMajorValue:
+ type: string
+ description: The value from Canis Major
diff --git a/validation-service/src/test/java/IntegrationTest.java b/validation-service/src/test/java/IntegrationTest.java
new file mode 100644
index 0000000..d556a4f
--- /dev/null
+++ b/validation-service/src/test/java/IntegrationTest.java
@@ -0,0 +1,114 @@
+package org.fiware.validation_service.integration;
+
+import io.cucumber.java.Before;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import org.fiware.validation_service.model.Discrepancy;
+import org.fiware.validation_service.model.ValidationResult;
+import org.awaitility.Awaitility;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+public class ValidationServiceStepDefinitions {
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ private static final String NGSILD_TENANT = "orion";
+ private static final String CONTEXT_BROKER_URL = "127.0.0.1:1026";
+ private static final String CANIS_MAJOR_URL = "127.0.0.1:4000";
+ private static final String VALIDATION_SERVICE_URL = "127.0.0.1:8080";
+
+ private String testEntityId;
+ private ValidationResult validationResult;
+ private HttpClient httpClient;
+
+ @Before
+ public void setup() {
+ httpClient = HttpClient.newHttpClient();
+ testEntityId = "urn:ngsi-ld:dlttxreceipt:" + System.currentTimeMillis();
+ }
+
+ @Given("Orion-LD and Canis Major are running")
+ public void orion_and_canis_major_are_running() {
+ Awaitility.await("Wait for Orion-LD")
+ .atMost(Duration.of(30, ChronoUnit.SECONDS))
+ .until(() -> isServiceRunning(CONTEXT_BROKER_URL, "/version"));
+
+ Awaitility.await("Wait for Canis Major")
+ .atMost(Duration.of(30, ChronoUnit.SECONDS))
+ .until(() -> isServiceRunning(CANIS_MAJOR_URL, "/health"));
+ }
+
+ @Given("A transaction entity exists in both systems")
+ public void transaction_entity_exists_in_both_systems() throws Exception {
+ // This would typically create or ensure a transaction entity exists
+ // For testing purposes, we could use a known entity ID
+ testEntityId = "urn:ngsi-ld:dlttxreceipt:0xa46d2e3b190d36fbb8af5d0a1c212d8036cf007e6ec4d1309904e052d25e5499";
+ }
+
+ @When("I validate the transaction entity")
+ public void validate_transaction_entity() throws Exception {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(String.format("http://%s/validation/transaction/%s",
+ VALIDATION_SERVICE_URL, testEntityId)))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build();
+
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(200, response.statusCode(), "Validation request should succeed");
+ validationResult = OBJECT_MAPPER.readValue(response.body(), ValidationResult.class);
+ }
+
+ @Then("The validation result should indicate consistency")
+ public void validation_result_should_indicate_consistency() {
+ assertTrue(validationResult.isValid(), "Transaction data should be consistent");
+ assertEquals(testEntityId, validationResult.getEntityId(), "Entity ID should match");
+ assertTrue(validationResult.getDiscrepancies().isEmpty(), "No discrepancies should be found");
+ }
+
+ @Then("The validation result should show discrepancies")
+ public void validation_result_should_show_discrepancies() {
+ assertFalse(validationResult.isValid(), "Transaction data should be inconsistent");
+ assertEquals(testEntityId, validationResult.getEntityId(), "Entity ID should match");
+ assertFalse(validationResult.getDiscrepancies().isEmpty(), "Discrepancies should be found");
+
+ // Optionally check specific discrepancies
+ List discrepancies = validationResult.getDiscrepancies();
+ discrepancies.forEach(discrepancy -> {
+ System.out.println("Field: " + discrepancy.getField());
+ System.out.println("Orion value: " + discrepancy.getOrionValue());
+ System.out.println("Canis Major value: " + discrepancy.getCanisMajorValue());
+ });
+ }
+
+ private boolean isServiceRunning(String host, String path) {
+ try {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(String.format("http://%s%s", host, path)))
+ .GET()
+ .build();
+
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString());
+
+ return response.statusCode() >= 200 && response.statusCode() < 300;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}