From e6100b05f6d5b0f7adef142fa3963de5aa5f58d6 Mon Sep 17 00:00:00 2001 From: Muhammad Auwal Date: Wed, 24 Jun 2026 20:53:03 +0100 Subject: [PATCH] feat(registry-service): implement contract registry microservice with bytecode verification (#355) --- .../contract-registry-service/Dockerfile | 14 +++++ src/entities/audit.entity.ts | 26 ++++++++ src/entities/contract.entity.ts | 39 ++++++++++++ src/entities/version.entity.ts | 26 ++++++++ src/registry/registry.controller.ts | 61 +++++++++++++++++++ src/registry/verification.service.ts | 41 +++++++++++++ 6 files changed, 207 insertions(+) create mode 100644 microservices/contract-registry-service/Dockerfile create mode 100644 src/entities/audit.entity.ts create mode 100644 src/entities/contract.entity.ts create mode 100644 src/entities/version.entity.ts create mode 100644 src/registry/registry.controller.ts create mode 100644 src/registry/verification.service.ts diff --git a/microservices/contract-registry-service/Dockerfile b/microservices/contract-registry-service/Dockerfile new file mode 100644 index 0000000..7f2b527 --- /dev/null +++ b/microservices/contract-registry-service/Dockerfile @@ -0,0 +1,14 @@ +FROM node:20-alpine AS builder +WORKDIR /usr/src/app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM node:20-alpine +WORKDIR /usr/src/app +COPY package*.json ./ +RUN npm ci --only=production +COPY --from=builder /usr/src/app/dist ./dist +EXPOSE 3000 +CMD ["node", "dist/main"] \ No newline at end of file diff --git a/src/entities/audit.entity.ts b/src/entities/audit.entity.ts new file mode 100644 index 0000000..2a98851 --- /dev/null +++ b/src/entities/audit.entity.ts @@ -0,0 +1,26 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn } from 'typeorm'; +import { SmartContract } from './contract.entity'; + +@Entity('security_audits') +export class SecurityAudit { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + auditorName: string; + + @Column() + reportUrl: string; + + @Column() + summary: string; + + @Column() + findingsCount: number; + + @ManyToOne(() => SmartContract, (c) => c.audits) + contract: SmartContract; + + @CreateDateColumn() + createdAt: Date; +} \ No newline at end of file diff --git a/src/entities/contract.entity.ts b/src/entities/contract.entity.ts new file mode 100644 index 0000000..e310cfe --- /dev/null +++ b/src/entities/contract.entity.ts @@ -0,0 +1,39 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm'; +import { ContractVersion } from './version.entity'; +import { SecurityAudit } from './audit.entity'; + +export enum ContractStatus { + VERIFIED = 'verified', + UNVERIFIED = 'unverified', + DEPRECATED = 'deprecated', +} + +@Entity('contracts') +export class SmartContract { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true }) + contractId: string; // Stellar/Soroban Contract Address (e.g., C...) + + @Column() + name: string; + + @Column({ type: 'enum', enum: ContractStatus, default: ContractStatus.UNVERIFIED }) + status: ContractStatus; + + @Column({ nullable: true }) + deprecationNotice: string; + + @OneToMany(() => ContractVersion, (v) => v.contract, { cascade: true }) + versions: ContractVersion[]; + + @OneToMany(() => SecurityAudit, (a) => a.contract, { cascade: true }) + audits: SecurityAudit[]; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} \ No newline at end of file diff --git a/src/entities/version.entity.ts b/src/entities/version.entity.ts new file mode 100644 index 0000000..e551186 --- /dev/null +++ b/src/entities/version.entity.ts @@ -0,0 +1,26 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn } from 'typeorm'; +import { SmartContract } from './contract.entity'; + +@Entity('contract_versions') +export class ContractVersion { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + versionStr: string; // e.g., "1.0.0" + + @Column() + wasmHash: string; // SHA256 hash of compiled WASM payload + + @Column({ nullable: true }) + sourceCodeUrl: string; // Reference link to verified GitHub commit + + @Column({ default: false }) + isVerified: boolean; + + @ManyToOne(() => SmartContract, (c) => c.versions) + contract: SmartContract; + + @CreateDateColumn() + createdAt: Date; +} \ No newline at end of file diff --git a/src/registry/registry.controller.ts b/src/registry/registry.controller.ts new file mode 100644 index 0000000..c804bde --- /dev/null +++ b/src/registry/registry.controller.ts @@ -0,0 +1,61 @@ +import { Controller, Post, Body, Param, Put, UploadedFile, UseInterceptors } from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { SmartContract, ContractStatus } from '../entities/contract.entity'; +import { ContractVersion } from '../entities/version.entity'; +import { VerificationService } from './verification.service'; + +@Controller('contracts') +export class RegistryController { + constructor( + @InjectRepository(SmartContract) private readonly contractRepo: Repository, + @InjectRepository(ContractVersion) private readonly versionRepo: Repository, + private readonly verificationService: VerificationService, + ) {} + + @Post('register') + async registerContract(@Body() body: { contractId: string; name: string; version: string }) { + const contract = this.contractRepo.create({ + contractId: body.contractId, + name: body.name, + status: ContractStatus.UNVERIFIED, + }); + return this.contractRepo.save(contract); + } + + @Post(':id/verify') + @UseInterceptors(FileInterceptor('wasm')) + async verifyVersion( + @Param('id') contractId: string, + @Body('versionStr') versionStr: string, + @UploadedFile() file: Express.Multer.File, + ) { + const contract = await this.contractRepo.findOneByOrFail({ contractId }); + const match = await this.verificationService.verifyContractBytecode(contractId, file.buffer); + + const version = this.versionRepo.create({ + versionStr, + wasmHash: this.verificationService.calculateHash(file.buffer), + isVerified: match, + contract, + }); + + await this.versionRepo.save(version); + + if (match) { + contract.status = ContractStatus.VERIFIED; + await this.contractRepo.save(contract); + } + + return { verified: match, hash: version.wasmHash }; + } + + @Put(':id/deprecate') + async deprecateContract(@Param('id') contractId: string, @Body('notice') notice: string) { + const contract = await this.contractRepo.findOneByOrFail({ contractId }); + contract.status = ContractStatus.DEPRECATED; + contract.deprecationNotice = notice; + return this.contractRepo.save(contract); + } +} \ No newline at end of file diff --git a/src/registry/verification.service.ts b/src/registry/verification.service.ts new file mode 100644 index 0000000..d4432cc --- /dev/null +++ b/src/registry/verification.service.ts @@ -0,0 +1,41 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { rpc, StrKey } from '@stellar/stellar-sdk'; +import * as crypto from 'crypto'; + +@Injectable() +export class VerificationService { + private rpcInstance: rpc.Server; + + constructor() { + // Falls back to Stellar testnet RPC endpoint + this.rpcInstance = new rpc.Server(process.env.STELLAR_RPC_URL || 'https://soroban-testnet.stellar.org'); + } + + /** + * Generates a deterministic SHA256 hash from raw file buffers + */ + calculateHash(buffer: Buffer): string { + return crypto.createHash('sha256').update(buffer).digest('hex'); + } + + /** + * Compares uploaded source compilation directly to live ledger states + */ + async verifyContractBytecode(contractId: string, uploadedWasm: Buffer): Promise { + if (!StrKey.isValidContractId(contractId)) { + throw new BadRequestException('Invalid Soroban Contract ID format.'); + } + + try { + // Fetch structural ledger ledger entry directly from network RPC + const contractData = await this.rpcInstance.getContractWasmByContractId(contractId); + + const onChainHash = Buffer.from(contractData.hash).toString('hex'); + const uploadedHash = this.calculateHash(uploadedWasm); + + return onChainHash === uploadedHash; + } catch (error) { + throw new BadRequestException(`Failed to cross-verify bytecode against network: ${error.message}`); + } + } +} \ No newline at end of file