From 03948854037de38ca13e217a30721c1e7a9e4aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Dan?= Date: Mon, 28 Aug 2023 11:51:01 +0200 Subject: [PATCH] feat: use receipt trie instead of transaction trie (#14) --- .env.example | 2 +- README.md | 2 +- package-lock.json | 14 ++--- package.json | 2 +- src/execute/execute.controller.spec.ts | 7 +-- src/execute/execute.dto.ts | 28 ++++------ src/execute/execute.errors.ts | 8 ++- src/execute/execute.processor.spec.ts | 21 ++++--- src/execute/execute.processor.ts | 44 ++++++++------- src/execute/execute.service.spec.ts | 11 ++-- src/execute/execute.service.ts | 7 +-- src/utils/index.ts | 6 ++ test/execute.e2e-spec.ts | 76 ++++++++------------------ 13 files changed, 101 insertions(+), 127 deletions(-) create mode 100644 src/utils/index.ts diff --git a/.env.example b/.env.example index 4db28ca..d4756fa 100644 --- a/.env.example +++ b/.env.example @@ -4,5 +4,5 @@ REDIS_HOST= REDIS_PORT= TOPOS_SUBNET_ENDPOINT= SUBNET_REGISTRATOR_CONTRACT_ADDRESS= -TOPOS_CORE_CONTRACT_ADDRESS= +TOPOS_CORE_PROXY_CONTRACT_ADDRESS= TRACING_OTEL_COLLECTOR_ENDPOINT= diff --git a/README.md b/README.md index d3ae5d6..3b503ff 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ REDIS_HOST= REDIS_PORT= TOPOS_SUBNET_ENDPOINT= SUBNET_REGISTRATOR_CONTRACT_ADDRESS= -TOPOS_CORE_CONTRACT_ADDRESS= +TOPOS_CORE_PROXY_CONTRACT_ADDRESS= ERC20_MESSAGING_CONTRACT_ADDRESS= ``` diff --git a/package-lock.json b/package-lock.json index 5ab436e..2b7692f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@opentelemetry/sdk-node": "^0.41.1", "@opentelemetry/sdk-trace-base": "^1.15.1", "@opentelemetry/semantic-conventions": "^1.15.1", - "@topos-protocol/topos-smart-contracts": "^1.1.2", + "@topos-protocol/topos-smart-contracts": "^1.2.0", "bcrypt": "^5.1.0", "bull": "^4.10.1", "class-transformer": "^0.5.1", @@ -3237,9 +3237,9 @@ } }, "node_modules/@topos-protocol/topos-smart-contracts": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@topos-protocol/topos-smart-contracts/-/topos-smart-contracts-1.1.2.tgz", - "integrity": "sha512-yhArAs1PdMbFYEV0Vi0YnpZ4oKxYK6P1oJwAAmbEPXk8zWCwMZyAxz2Wn7KEn64qJZikPshF2w24BMzX1lZh1g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@topos-protocol/topos-smart-contracts/-/topos-smart-contracts-1.2.0.tgz", + "integrity": "sha512-0xw8BOhKyUF3N1swopBGvvKxD1D4Qmkp+JF0O5FclWewDUDH/N+HiIiFn+QJuCvq1jhaRXhW+0Ys3dTH89/YNA==", "dependencies": { "@openzeppelin/contracts": "^4.8.3", "ethers": "^5.7.2", @@ -13412,9 +13412,9 @@ "dev": true }, "@topos-protocol/topos-smart-contracts": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@topos-protocol/topos-smart-contracts/-/topos-smart-contracts-1.1.2.tgz", - "integrity": "sha512-yhArAs1PdMbFYEV0Vi0YnpZ4oKxYK6P1oJwAAmbEPXk8zWCwMZyAxz2Wn7KEn64qJZikPshF2w24BMzX1lZh1g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@topos-protocol/topos-smart-contracts/-/topos-smart-contracts-1.2.0.tgz", + "integrity": "sha512-0xw8BOhKyUF3N1swopBGvvKxD1D4Qmkp+JF0O5FclWewDUDH/N+HiIiFn+QJuCvq1jhaRXhW+0Ys3dTH89/YNA==", "requires": { "@openzeppelin/contracts": "^4.8.3", "ethers": "^5.7.2", diff --git a/package.json b/package.json index 1ff95f2..148537a 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@opentelemetry/sdk-node": "^0.41.1", "@opentelemetry/sdk-trace-base": "^1.15.1", "@opentelemetry/semantic-conventions": "^1.15.1", - "@topos-protocol/topos-smart-contracts": "^1.1.2", + "@topos-protocol/topos-smart-contracts": "^1.2.0", "bcrypt": "^5.1.0", "bull": "^4.10.1", "class-transformer": "^0.5.1", diff --git a/src/execute/execute.controller.spec.ts b/src/execute/execute.controller.spec.ts index 962aa26..01902fc 100644 --- a/src/execute/execute.controller.spec.ts +++ b/src/execute/execute.controller.spec.ts @@ -7,12 +7,11 @@ import { ExecuteServiceV1 } from './execute.service' import { Observable, observable } from 'rxjs' const validExecuteDto: ExecuteDto = { - indexOfDataInTxRaw: 4, + logIndexes: [], messagingContractAddress: '', + receiptTrieRoot: '', + receiptTrieMerkleProof: '', subnetId: '', - txRaw: '', - txTrieRoot: '', - txTrieMerkleProof: '', } describe('ExecuteController', () => { diff --git a/src/execute/execute.dto.ts b/src/execute/execute.dto.ts index 19c72ae..9398833 100644 --- a/src/execute/execute.dto.ts +++ b/src/execute/execute.dto.ts @@ -1,6 +1,5 @@ import { ApiProperty } from '@nestjs/swagger' import { - IsDefined, IsEthereumAddress, IsNotEmpty, IsNumber, @@ -9,42 +8,35 @@ import { export class ExecuteDto { @ApiProperty({ - description: - 'The raw transaction of a cross-subnet message from the sending subnet', + description: 'The id of the receiving subnet', }) @IsNotEmpty() @IsString() - txRaw: string - - @ApiProperty({ - description: 'The index of the data binary in the raw transaction', - }) - @IsDefined() - @IsNumber() - indexOfDataInTxRaw: number + subnetId: string @ApiProperty({ - description: 'The id of the receiving subnet', + description: + 'The array of indexes that the messaging contract should use to validate the cross-subnet message semantically', }) @IsNotEmpty() - @IsString() - subnetId: string + @IsNumber({}, { each: true }) + logIndexes: number[] @ApiProperty({ description: - 'The root of the transaction trie including the cross-subnet message transaction from the sending subnet', + 'The root of the receipt trie including the cross-subnet message tx receipt from the sending subnet', }) @IsNotEmpty() @IsString() - txTrieRoot: string + receiptTrieRoot: string @ApiProperty({ description: - 'The merkle proof proving the inclusion of the cross-subnet message transaction from the sending subnet in the certified transaction trie', + 'The merkle proof proving the inclusion of the cross-subnet message tx receipt from the sending subnet in the certified receipt trie', }) @IsNotEmpty() @IsString() - txTrieMerkleProof: string + receiptTrieMerkleProof: string @ApiProperty({ description: 'The address of the messaging contract', diff --git a/src/execute/execute.errors.ts b/src/execute/execute.errors.ts index b4f3d53..235960e 100644 --- a/src/execute/execute.errors.ts +++ b/src/execute/execute.errors.ts @@ -11,6 +11,10 @@ export enum CONTRACT_ERRORS { } export enum QUEUE_ERRORS { - JOB_NOT_FOUND = 'Job // A job with the provided id could not be found!', - REDIS_NOT_AVAILABLE = 'Redis // Could not connect to Redis!', + JOB_NOT_FOUND = 'Queue // A job with the provided id could not be found!', + REDIS_NOT_AVAILABLE = 'Queue // Could not connect to Redis!', +} + +export enum JOB_ERRORS { + MISSING_CERTIFICATE = 'Job // Could not find the related certificate!', } diff --git a/src/execute/execute.processor.spec.ts b/src/execute/execute.processor.spec.ts index c41f8e8..36b8dad 100644 --- a/src/execute/execute.processor.spec.ts +++ b/src/execute/execute.processor.spec.ts @@ -9,17 +9,17 @@ import { ExecutionProcessorV1 } from './execute.processor' const VALID_PRIVATE_KEY = '0xc6cbd7d76bc5baca530c875663711b947efa6a86a900a9e8645ce32e5821484e' -const TOPOS_CORE_CONTRACT_ADDRESS = '0x1D7b9f9b1FF6cf0A3BEB0F84fA6F8628E540E97F' +const TOPOS_CORE_PROXY_CONTRACT_ADDRESS = + '0x1D7b9f9b1FF6cf0A3BEB0F84fA6F8628E540E97F' const TOPOS_SUBNET_ENDPOINT = 'topos-subnet-endpoint' const validExecuteJob: Partial> = { data: { - indexOfDataInTxRaw: 4, + logIndexes: [], messagingContractAddress: '', + receiptTrieRoot: '', + receiptTrieMerkleProof: '', subnetId: 'id', - txRaw: '', - txTrieRoot: '', - txTrieMerkleProof: '', }, progress: jest.fn(), } @@ -52,8 +52,8 @@ describe('ExecuteProcessor', () => { switch (key) { case 'PRIVATE_KEY': return VALID_PRIVATE_KEY - case 'TOPOS_CORE_CONTRACT_ADDRESS': - return TOPOS_CORE_CONTRACT_ADDRESS + case 'TOPOS_CORE_PROXY_CONTRACT_ADDRESS': + return TOPOS_CORE_PROXY_CONTRACT_ADDRESS case 'TOPOS_SUBNET_ENDPOINT': return TOPOS_SUBNET_ENDPOINT } @@ -98,10 +98,9 @@ describe('ExecuteProcessor', () => { expect(validExecuteJob.progress).toHaveBeenCalledWith(50) expect(contractMock.execute).toHaveBeenCalledWith( - validExecuteJob.data.indexOfDataInTxRaw, - validExecuteJob.data.txTrieMerkleProof, - validExecuteJob.data.txRaw, - validExecuteJob.data.txTrieRoot, + validExecuteJob.data.logIndexes, + validExecuteJob.data.receiptTrieRoot, + validExecuteJob.data.receiptTrieMerkleProof, { gasLimit: 4_000_000, } diff --git a/src/execute/execute.processor.ts b/src/execute/execute.processor.ts index 5dab0bc..0263c4f 100644 --- a/src/execute/execute.processor.ts +++ b/src/execute/execute.processor.ts @@ -25,9 +25,14 @@ import { ethers, providers } from 'ethers' import { ExecuteDto } from './execute.dto' import { CONTRACT_ERRORS, + JOB_ERRORS, PROVIDER_ERRORS, WALLET_ERRORS, } from './execute.errors' +import { sanitizeURLProtocol } from '../utils' + +const UNDEFINED_CERTIFICATE_ID = + '0x0000000000000000000000000000000000000000000000000000000000000000' @Processor('execute') export class ExecutionProcessorV1 { @@ -38,16 +43,15 @@ export class ExecutionProcessorV1 { @Process('execute') async execute(job: Job) { const { - indexOfDataInTxRaw, + logIndexes, messagingContractAddress, + receiptTrieMerkleProof, + receiptTrieRoot, subnetId, - txRaw, - txTrieMerkleProof, - txTrieRoot, } = job.data const toposCoreContractAddress = this.configService.get( - 'TOPOS_CORE_CONTRACT_ADDRESS' + 'TOPOS_CORE_PROXY_CONTRACT_ADDRESS' ) const receivingSubnetEndpoint = @@ -72,28 +76,30 @@ export class ExecutionProcessorV1 { wallet )) as ToposMessaging - this.logger.debug(`Trie root: ${txTrieRoot}`) + this.logger.debug(`Trie root: ${receiptTrieRoot}`) - let certId = - '0x0000000000000000000000000000000000000000000000000000000000000000' + let certId = UNDEFINED_CERTIFICATE_ID let i = 1 - while ( - certId == - '0x0000000000000000000000000000000000000000000000000000000000000000' - ) { + + while (certId == UNDEFINED_CERTIFICATE_ID && i < 40) { this.logger.debug(`Waiting for cert to be imported (${i})`) - certId = await toposCoreContract.txRootToCertId(txTrieRoot) + certId = await toposCoreContract.receiptRootToCertId(receiptTrieRoot) this.logger.debug(`Cert id: ${certId}`) + await new Promise((r) => setTimeout(r, 1000)) i++ } + if (certId == UNDEFINED_CERTIFICATE_ID) { + await job.moveToFailed({ message: JOB_ERRORS.MISSING_CERTIFICATE }) + return + } + await job.progress(50) const tx = await messagingContract.execute( - indexOfDataInTxRaw, - txTrieMerkleProof, - txRaw, - txTrieRoot, + logIndexes, + receiptTrieMerkleProof, + receiptTrieRoot, { gasLimit: 4_000_000, } @@ -110,7 +116,7 @@ export class ExecutionProcessorV1 { 'TOPOS_SUBNET_ENDPOINT' ) const toposCoreContractAddress = this.configService.get( - 'TOPOS_CORE_CONTRACT_ADDRESS' + 'TOPOS_CORE_PROXY_CONTRACT_ADDRESS' ) const subnetRegistratorContractAddress = this.configService.get( 'SUBNET_REGISTRATOR_CONTRACT_ADDRESS' @@ -143,7 +149,7 @@ export class ExecutionProcessorV1 { private _createProvider(endpoint: string) { return new Promise((resolve, reject) => { const provider = new ethers.providers.WebSocketProvider( - `ws://${endpoint}/ws` + sanitizeURLProtocol('ws', `${endpoint}/ws`) ) // Fix: Timeout to leave time to errors to be asynchronously caught diff --git a/src/execute/execute.service.spec.ts b/src/execute/execute.service.spec.ts index 055d199..2bbd070 100644 --- a/src/execute/execute.service.spec.ts +++ b/src/execute/execute.service.spec.ts @@ -8,12 +8,11 @@ import { ConfigService } from '@nestjs/config' import { first, firstValueFrom, lastValueFrom } from 'rxjs' const validExecuteDto: ExecuteDto = { - indexOfDataInTxRaw: 4, + logIndexes: [], messagingContractAddress: '', + receiptTrieRoot: '', + receiptTrieMerkleProof: '', subnetId: '', - txRaw: '', - txTrieRoot: '', - txTrieMerkleProof: '', } const VALID_PRIVATE_KEY = @@ -131,7 +130,7 @@ describe('ExecuteService', () => { const job: Partial = { id: jobId, failedReason: 'errorMock', - finished: jest.fn().mockRejectedValueOnce(''), + finished: jest.fn().mockRejectedValueOnce('errorMock'), } resolve(job) }) @@ -139,7 +138,7 @@ describe('ExecuteService', () => { await expect( lastValueFrom(executeService.subscribeToJobById(jobId)) - ).rejects.toStrictEqual({ data: 'errorMock' }) + ).rejects.toStrictEqual('errorMock') }) }) }) diff --git a/src/execute/execute.service.ts b/src/execute/execute.service.ts index 6c7e12c..ce9b98d 100644 --- a/src/execute/execute.service.ts +++ b/src/execute/execute.service.ts @@ -64,12 +64,9 @@ export class ExecuteServiceV1 { subscriber.complete() }) .catch((error) => { - const messageEvent: MessageEvent = { - data: job.failedReason, - } this.logger.debug(`Job failed!`) - this.logger.debug(job.failedReason) - subscriber.error(messageEvent) + this.logger.debug(error) + subscriber.error(error) subscriber.complete() }) }) diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..a735a7e --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,6 @@ +export function sanitizeURLProtocol(protocol: 'ws' | 'http', endpoint: string) { + return endpoint.indexOf('localhost') > -1 || + endpoint.indexOf('127.0.0.1') > -1 + ? `${protocol}://${endpoint}` + : `${protocol}s://${endpoint}` +} diff --git a/test/execute.e2e-spec.ts b/test/execute.e2e-spec.ts index 2036086..98df39a 100644 --- a/test/execute.e2e-spec.ts +++ b/test/execute.e2e-spec.ts @@ -8,12 +8,11 @@ import { ExecuteDto } from '../src/execute/execute.dto' import { ExecuteServiceV1 } from '../src/execute/execute.service' const validExecuteDto: ExecuteDto = { - indexOfDataInTxRaw: 4, + logIndexes: [], messagingContractAddress: '0x3B5aCC9B6e58543512828EFAe26B29B7292c8273', + receiptTrieRoot: 'txTrieRoot', + receiptTrieMerkleProof: 'txTrieMerkleProof', subnetId: 'subnetId', - txRaw: 'txRaw', - txTrieRoot: 'txTrieRoot', - txTrieMerkleProof: 'txTrieMerkleProof', } describe('Execute with ❌ auth (e2e)', () => { @@ -107,10 +106,10 @@ describe('Execute with ✅ auth (e2e)', () => { }) }) - it('/execute (POST) should reject with invalid dto (invalid indexOfDataInTxRaw)', () => { + it('/execute (POST) should reject with invalid dto (invalid logIndexes)', () => { const invalidExecuteDto = { ...validExecuteDto, - indexOfDataInTxRaw: 'invalid', + logIndexes: ['invalid'], } return request(app.getHttpServer()) .post('/execute') @@ -118,22 +117,22 @@ describe('Execute with ✅ auth (e2e)', () => { .expect({ statusCode: 400, message: [ - 'indexOfDataInTxRaw must be a number conforming to the specified constraints', + 'each value in logIndexes must be a number conforming to the specified constraints', ], error: 'Bad Request', }) }) - it('/execute (POST) should reject with invalid dto (missing indexOfDataInTxRaw)', () => { - const { indexOfDataInTxRaw, ...invalidExecuteDto } = validExecuteDto + it('/execute (POST) should reject with invalid dto (missing logIndexes)', () => { + const { logIndexes, ...invalidExecuteDto } = validExecuteDto return request(app.getHttpServer()) .post('/execute') .send(invalidExecuteDto) .expect({ statusCode: 400, message: [ - 'indexOfDataInTxRaw should not be null or undefined', - 'indexOfDataInTxRaw must be a number conforming to the specified constraints', + 'each value in logIndexes must be a number conforming to the specified constraints', + 'logIndexes should not be empty', ], error: 'Bad Request', }) @@ -166,88 +165,61 @@ describe('Execute with ✅ auth (e2e)', () => { }) }) - it('/execute (POST) should reject with invalid dto (invalid txRaw)', () => { + it('/execute (POST) should reject with invalid dto (invalid receiptTrieRoot)', () => { const invalidExecuteDto = { ...validExecuteDto, - txRaw: 1, + receiptTrieRoot: 1, } return request(app.getHttpServer()) .post('/execute') .send(invalidExecuteDto) .expect({ statusCode: 400, - message: ['txRaw must be a string'], + message: ['receiptTrieRoot must be a string'], error: 'Bad Request', }) }) - it('/execute (POST) should reject with invalid dto (missing txRaw)', () => { - const { txRaw, ...invalidExecuteDto } = validExecuteDto - return request(app.getHttpServer()) - .post('/execute') - .send(invalidExecuteDto) - .expect({ - statusCode: 400, - message: ['txRaw must be a string', 'txRaw should not be empty'], - error: 'Bad Request', - }) - }) - - it('/execute (POST) should reject with invalid dto (invalid txTrieRoot)', () => { - const invalidExecuteDto = { - ...validExecuteDto, - txTrieRoot: 1, - } - return request(app.getHttpServer()) - .post('/execute') - .send(invalidExecuteDto) - .expect({ - statusCode: 400, - message: ['txTrieRoot must be a string'], - error: 'Bad Request', - }) - }) - - it('/execute (POST) should reject with invalid dto (missing txTrieRoot)', () => { - const { txTrieRoot, ...invalidExecuteDto } = validExecuteDto + it('/execute (POST) should reject with invalid dto (missing receiptTrieRoot)', () => { + const { receiptTrieRoot, ...invalidExecuteDto } = validExecuteDto return request(app.getHttpServer()) .post('/execute') .send(invalidExecuteDto) .expect({ statusCode: 400, message: [ - 'txTrieRoot must be a string', - 'txTrieRoot should not be empty', + 'receiptTrieRoot must be a string', + 'receiptTrieRoot should not be empty', ], error: 'Bad Request', }) }) - it('/execute (POST) should reject with invalid dto (invalid txTrieMerkleProof)', () => { + it('/execute (POST) should reject with invalid dto (invalid receiptTrieMerkleProof)', () => { const invalidExecuteDto = { ...validExecuteDto, - txTrieMerkleProof: 1, + receiptTrieMerkleProof: 1, } return request(app.getHttpServer()) .post('/execute') .send(invalidExecuteDto) .expect({ statusCode: 400, - message: ['txTrieMerkleProof must be a string'], + message: ['receiptTrieMerkleProof must be a string'], error: 'Bad Request', }) }) - it('/execute (POST) should reject with invalid dto (missing txTrieMerkleProof)', () => { - const { txTrieMerkleProof, ...invalidExecuteDto } = validExecuteDto + it('/execute (POST) should reject with invalid dto (missing receiptTrieMerkleProof)', () => { + const { receiptTrieMerkleProof, ...invalidExecuteDto } = validExecuteDto return request(app.getHttpServer()) .post('/execute') .send(invalidExecuteDto) .expect({ statusCode: 400, message: [ - 'txTrieMerkleProof must be a string', - 'txTrieMerkleProof should not be empty', + 'receiptTrieMerkleProof must be a string', + 'receiptTrieMerkleProof should not be empty', ], error: 'Bad Request', })