Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions microservices/contract-registry-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
26 changes: 26 additions & 0 deletions src/entities/audit.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
39 changes: 39 additions & 0 deletions src/entities/contract.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
26 changes: 26 additions & 0 deletions src/entities/version.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
61 changes: 61 additions & 0 deletions src/registry/registry.controller.ts
Original file line number Diff line number Diff line change
@@ -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<SmartContract>,
@InjectRepository(ContractVersion) private readonly versionRepo: Repository<ContractVersion>,
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);
}
}
41 changes: 41 additions & 0 deletions src/registry/verification.service.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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}`);
}
}
}
Loading