Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: CORD Blockchain Integration for Identity, Schema, and Credential Anchoring in Sunbird RC #354

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,22 @@ SCHEMA_BASE_URL=http://credential-schema:3333
CREDENTIAL_SERVICE_BASE_URL=https://example.com/credentials
JWKS_URI=
ENABLE_AUTH=false


# Anchor to cord block chain
# Flag to enable/disable anchoring to Cord blockchain
ANCHOR_TO_CORD=true

# Base URL for Issuer Agent
# This is the service responsible for issuing credentials
# Example: https://<ISSUER_AGENT_IP>/api/v1
ISSUER_AGENT_BASE_URL=https://<ISSUER_AGENT_IP>/api/v1

# Base URL for Verification Middleware
# This service is responsible for verifying credentials
# Example: https://<VERIFICATION_MIDDLEWARE_IP>/api/v1/verify
VERIFICATION_MIDDLEWARE_BASE_URL=https://<VERIFICATION_MIDDLEWARE_IP>/api/v1/verify

# Additional Resources:
# - For more details on Issuer Agent, visit: https://github.com/dhiway/issuer-agent
# - For more details on Verification Middleware, visit: https://github.com/dhiway/verification-middleware
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ services:
- JWKS_URI=${JWKS_URI}
- ENABLE_AUTH=${ENABLE_AUTH}
- WEB_DID_BASE_URL=${WEB_DID_BASE_URL}
- ANCHOR_TO_CORD=${ANCHOR_TO_CORD}
- ISSUER_AGENT_BASE_URL=${ISSUER_AGENT_BASE_URL}
healthcheck:
test:
[ "CMD-SHELL", "curl -f http://localhost:3332/health || exit 1" ]
Expand All @@ -263,6 +265,8 @@ services:
- IDENTITY_BASE_URL=${IDENTITY_BASE_URL}
- JWKS_URI=${JWKS_URI}
- ENABLE_AUTH=${ENABLE_AUTH}
- ANCHOR_TO_CORD=${ANCHOR_TO_CORD}
- ISSUER_AGENT_BASE_URL=${ISSUER_AGENT_BASE_URL}
healthcheck:
test:
[ "CMD-SHELL", "curl -f http://localhost:3333/health || exit 1" ]
Expand All @@ -288,6 +292,9 @@ services:
- JWKS_URI=${JWKS_URI}
- ENABLE_AUTH=${ENABLE_AUTH}
- QR_TYPE=${QR_TYPE}
- ANCHOR_TO_CORD=${ANCHOR_TO_CORD}
- ISSUER_AGENT_BASE_URL=${ISSUER_AGENT_BASE_URL}
- VERIFICATION_MIDDLEWARE_BASE_URL=${VERIFICATION_MIDDLEWARE_BASE_URL}
healthcheck:
test:
[ "CMD-SHELL", "curl -f http://localhost:3000/health || exit 1" ]
Expand Down
12 changes: 12 additions & 0 deletions services/credential-schema/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,15 @@ IDENTITY_BASE_URL= # URL of the identity service to facilitate DID creation
# Service VARS
PORT=3000
SCHEMA_BASE_URL=


# Flag to enable/disable anchoring to Cord blockchain
ANCHOR_TO_CORD=false

# Base URL for Issuer Agent
# This is the service responsible for issuing credentials
# Example: https://<ISSUER_AGENT_IP>/api/v1
ISSUER_AGENT_BASE_URL=https://<ISSUER_AGENT_IP>/api/v1

# Additional Resources:
# - For more details on Issuer Agent, visit: https://github.com/dhiway/issuer-agent
2 changes: 2 additions & 0 deletions services/credential-schema/docker-compose-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ services:
DATABASE_URL: postgres://postgres:postgres@db-test:5432/postgres
IDENTITY_BASE_URL: "http://identity-service:3332"
ENABLE_AUTH: "false"
ANCHOR_TO_CORD: "${ANCHOR_TO_CORD}"
ISSUER_AGENT_BASE_URL: "${ISSUER_AGENT_BASE_URL}"
networks:
test:
rcw-test:
Expand Down
2 changes: 2 additions & 0 deletions services/credential-schema/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ services:
DATABASE_URL: postgres://postgres:postgres@db:5432/postgres
IDENTITY_BASE_URL: "http://identity-service:3332"
ENABLE_AUTH: "false"
ANCHOR_TO_CORD: "${ANCHOR_TO_CORD}"
ISSUER_AGENT_BASE_URL: "${ISSUER_AGENT_BASE_URL}"
networks:
rcw-test:
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "BlockchainStatus" AS ENUM ('PENDING', 'ANCHORED', 'FAILED');

-- AlterTable
ALTER TABLE "VerifiableCredentialSchema" ADD COLUMN "blockchainStatus" "BlockchainStatus" NOT NULL DEFAULT 'PENDING';
7 changes: 7 additions & 0 deletions services/credential-schema/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ enum SchemaStatus {
REVOKED
}

enum BlockchainStatus {
PENDING
ANCHORED
FAILED
}

model VerifiableCredentialSchema {
id String
type String
Expand All @@ -35,6 +41,7 @@ model VerifiableCredentialSchema {
tags String[]
status SchemaStatus @default(DRAFT)
deprecatedId String?
blockchainStatus BlockchainStatus @default(PENDING)

@@id([id, version])
@@index([type], type: Hash)
Expand Down
4 changes: 4 additions & 0 deletions services/credential-schema/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { PrismaHealthIndicator } from './utils/prisma.health';
import { PrismaClient } from '@prisma/client';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth/auth.guard';
import { AnchorCordService } from './schema/implementations/anchor-cord.service';
import { BlockchainAnchorFactory } from './schema/factories/blockchain-anchor.factory';

@Module({
imports: [
Expand All @@ -32,6 +34,8 @@ import { AuthGuard } from './auth/auth.guard';
UtilsService,
PrismaHealthIndicator,
PrismaClient,
AnchorCordService,
BlockchainAnchorFactory,
{
provide: APP_GUARD,
useClass: AuthGuard,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { SchemaService } from '../schema/schema.service';
import { HttpModule } from '@nestjs/axios';
import { UtilsService } from '../utils/utils.service';
import { PrismaClient } from '@prisma/client';
import { BlockchainAnchorFactory } from 'src/schema/factories/blockchain-anchor.factory';
import { AnchorCordService } from 'src/schema/implementations/anchor-cord.service';

@Module({
imports: [HttpModule],
Expand All @@ -15,6 +17,8 @@ import { PrismaClient } from '@prisma/client';
ValidateTemplateService,
SchemaService,
UtilsService,
BlockchainAnchorFactory,
AnchorCordService,
],
controllers: [RenderingTemplatesController],
})
Expand Down
114 changes: 114 additions & 0 deletions services/credential-schema/src/schema/anchor-cord-service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SchemaService } from './schema.service';
import { BlockchainAnchorFactory } from './factories/blockchain-anchor.factory';
import { PrismaClient } from '@prisma/client';
import { CreateCredentialDTO } from './dto/create-credentials.dto';
import { UtilsService } from '../utils/utils.service';
import { generateCredentialSchemaTestBody } from './schema.fixtures';
import { BadRequestException, InternalServerErrorException } from '@nestjs/common';

describe('SchemaService - createCredentialSchema', () => {
let service: SchemaService;
let blockchainFactory: BlockchainAnchorFactory;
let utilsService: UtilsService;

const mockBlockchainService = {
anchorSchema: jest.fn(),
};

const mockPrismaService = {
verifiableCredentialSchema: {
create: jest.fn(),
findMany: jest.fn(),
},
};

const mockUtilsService = {
generateDID: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SchemaService,
{
provide: BlockchainAnchorFactory,
useValue: {
getAnchorService: jest.fn(() => mockBlockchainService),
},
},
{
provide: PrismaClient,
useValue: mockPrismaService,
},
{
provide: UtilsService,
useValue: mockUtilsService,
},
],
}).compile();

service = module.get<SchemaService>(SchemaService);
blockchainFactory = module.get<BlockchainAnchorFactory>(BlockchainAnchorFactory);
utilsService = module.get<UtilsService>(UtilsService);
});

afterEach(() => {
jest.clearAllMocks(); // Clear mocks after each test
});

afterAll(async () => {
await mockPrismaService.verifiableCredentialSchema.create.mockClear();
});

it('should verify ANCHOR_TO_CORD is true', () => {
const anchorToCord = process.env.ANCHOR_TO_CORD;
const isTrue = anchorToCord?.toLowerCase().trim() === 'true';
expect(isTrue).toBe(true);
});

it('should verify ISSUER_AGENT_BASE_URL is a valid URL', () => {
const isValidUrl = (url: string) => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
expect(isValidUrl(process.env.ISSUER_AGENT_BASE_URL)).toBe(true);
});

it('should successfully anchor a credential schema to the blockchain', async () => {
const mockRequestBody = generateCredentialSchemaTestBody();
const mockResponse = { schemaId: 'schema-id-blockchain' };

mockBlockchainService.anchorSchema.mockResolvedValueOnce(mockResponse);
mockPrismaService.verifiableCredentialSchema.create.mockResolvedValueOnce({
...mockRequestBody.schema,
blockchainStatus: 'ANCHORED',
});

const result = await service.createCredentialSchema(mockRequestBody);

expect(blockchainFactory.getAnchorService).toHaveBeenCalledWith('cord');
expect(mockBlockchainService.anchorSchema).toHaveBeenCalledWith(mockRequestBody.schema);
expect(result).toBeDefined();
expect(result.schema.id).toEqual(mockResponse.schemaId);
expect(result.blockchainStatus).toEqual('ANCHORED');
});

it('should throw an error if blockchain anchoring fails', async () => {
const mockRequestBody = generateCredentialSchemaTestBody();

mockBlockchainService.anchorSchema.mockRejectedValueOnce(
new InternalServerErrorException('Blockchain anchoring failed')
);
await expect(service.createCredentialSchema(mockRequestBody)).rejects.toThrow(
InternalServerErrorException
);
});



});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Prisma, VerifiableCredentialSchema } from '@prisma/client';
import { Prisma, VerifiableCredentialSchema ,BlockchainStatus} from '@prisma/client';

// represents the schema stored in Prisma
export class VCItem implements VerifiableCredentialSchema {
Expand Down Expand Up @@ -34,4 +34,6 @@ export class VCItem implements VerifiableCredentialSchema {
createdBy: string;
updatedBy: string;
deprecatedId: string;
@ApiProperty({ enum: BlockchainStatus, description: 'Blockchain status' })
blockchainStatus: BlockchainStatus | null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { AnchorCordService } from '../implementations/anchor-cord.service';
import { BlockchainAnchor } from '../interfaces/blockchain_anchor.interface';

/**
* Factory class to dynamically resolve the appropriate BlockchainAnchor service.
* It uses the specified method to determine which implementation to return.
*/
@Injectable()
export class BlockchainAnchorFactory {
/**
* Constructor for the BlockchainAnchorFactory.
* @param cordService - An instance of AnchorCordService, which handles CORD-specific anchoring logic.
*/
constructor(private readonly cordService: AnchorCordService) {}

/**
* Resolves the appropriate BlockchainAnchor service based on the provided method.
* @param method - The blockchain method (e.g., 'cord').
* @returns The service instance corresponding to the specified method or null if no method is provided.
* @throws
*/
getAnchorService(method?: string): BlockchainAnchor | null {
// If no method is specified, return null to indicate no anchoring is required
if (!method) {
return null;
}

// Determine the appropriate service implementation based on the method
switch (method) {
case 'cord':
// Return the CORD-specific implementation
return this.cordService;
default:
throw new BadRequestException(`Unsupported blockchain method: ${method}`);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { BlockchainAnchor } from '../interfaces/blockchain_anchor.interface';
import {BadRequestException} from '@nestjs/common';
@Injectable()
export class AnchorCordService implements BlockchainAnchor {
private readonly logger = new Logger(AnchorCordService.name);

constructor(private readonly httpService: HttpService) {}

async anchorSchema(body: any): Promise<any> {
try {
const response = await this.httpService.axiosRef.post(
`${process.env.ISSUER_AGENT_BASE_URL}/schema`,
body,
);
return response.data.result;
} catch (err) {
const errorDetails = {
message: err.message,
status: err.response?.status,
statusText: err.response?.statusText,
data: err.response?.data,
headers: err.response?.headers,
request: err.config,
};

this.logger.error(
'Error anchoring schema to Cord blockchain',
errorDetails,
);
throw new InternalServerErrorException(
'Failed to anchor schema to Cord blockchain',
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface BlockchainAnchor {
/**
* Anchors a Scheam to the blockchain.
* @param body The request payload for anchoring.
* @returns The anchored Schema or related data.
*/
anchorSchema(body: any): Promise<any>;
}

2 changes: 2 additions & 0 deletions services/credential-schema/src/schema/schema.fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const testSchemaRespose1: VerifiableCredentialSchema = {
tags: ['degree', 'computer science', 'bachelor'],
status: 'DRAFT',
deprecatedId: null,
blockchainStatus: null
};

export const testSchemaRespose2: VerifiableCredentialSchema = {
Expand All @@ -104,4 +105,5 @@ export const testSchemaRespose2: VerifiableCredentialSchema = {
tags: ['certification', 'blockchain', 'expert'],
status: 'DRAFT',
deprecatedId: null,
blockchainStatus: null,
};
4 changes: 3 additions & 1 deletion services/credential-schema/src/schema/schema.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { SchemaService } from './schema.service';
import { HttpModule } from '@nestjs/axios';
import { UtilsService } from '../utils/utils.service';
import { PrismaClient } from '@prisma/client';
import { AnchorCordService } from './implementations/anchor-cord.service';
import { BlockchainAnchorFactory } from './factories/blockchain-anchor.factory';

@Module({
imports: [HttpModule],
controllers: [SchemaController],
providers: [SchemaService, PrismaClient, UtilsService],
providers: [SchemaService, PrismaClient, UtilsService,AnchorCordService,BlockchainAnchorFactory],
})
export class SchemaModule {}
Loading