From bb61c6b0a55f7629d9dcdb3f3530c67f34f8b881 Mon Sep 17 00:00:00 2001 From: Samuel Ojietohamen Date: Wed, 11 Sep 2024 18:31:00 +0100 Subject: [PATCH] feat: Add silent block encoding and decoding --- package-lock.json | 17 +--- package.json | 3 +- src/main.ts | 7 +- src/silent-blocks/silent-blocks.service.ts | 95 +++++++++++++++------- 4 files changed, 73 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4478a51..e45f819 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@nestjs/platform-express": "^10.3.7", "@nestjs/platform-socket.io": "^10.3.7", "@nestjs/schedule": "^3.0.0", - "@nestjs/swagger": "^7.4.0", + "@nestjs/swagger": "^7.3.1", "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.3.7", "axios": "^1.7.2", @@ -27,7 +27,6 @@ "rxjs": "^7.2.0", "secp256k1": "^5.0.0", "sqlite3": "^5.1.7", - "swagger-ui-express": "^5.0.1", "typeorm": "^0.3.20" }, "devDependencies": { @@ -11175,20 +11174,6 @@ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==" }, - "node_modules/swagger-ui-express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", - "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", - "dependencies": { - "swagger-ui-dist": ">=5.0.0" - }, - "engines": { - "node": ">= v0.10.32" - }, - "peerDependencies": { - "express": ">=4.0.0 || >=5.0.0-beta" - } - }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", diff --git a/package.json b/package.json index 9a4bd75..b02ab7a 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@nestjs/platform-express": "^10.3.7", "@nestjs/platform-socket.io": "^10.3.7", "@nestjs/schedule": "^3.0.0", - "@nestjs/swagger": "^7.4.0", + "@nestjs/swagger": "^7.3.1", "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.3.7", "axios": "^1.7.2", @@ -43,7 +43,6 @@ "rxjs": "^7.2.0", "secp256k1": "^5.0.0", "sqlite3": "^5.1.7", - "swagger-ui-express": "^5.0.1", "typeorm": "^0.3.20" }, "devDependencies": { diff --git a/src/main.ts b/src/main.ts index 6a5cbc6..e5c3179 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,7 +3,6 @@ import { AppModule } from '@/app.module'; import { ConfigService } from '@nestjs/config'; import { NestExpressApplication } from '@nestjs/platform-express'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import * as packageJson from 'package.json'; declare const module: any; @@ -16,8 +15,10 @@ async function bootstrap() { // Swagger Options const options = new DocumentBuilder() .setTitle('Silent Payment Indexer API') - .setDescription(packageJson.description) - .setVersion(packageJson.version) + .setDescription( + 'An indexer for serving bitcoin silent payment indexes!!', + ) + .setVersion('0.0.1') .build(); const document = SwaggerModule.createDocument(app, options); SwaggerModule.setup(`/docs`, app, document); diff --git a/src/silent-blocks/silent-blocks.service.ts b/src/silent-blocks/silent-blocks.service.ts index f566a8d..0fb62cc 100644 --- a/src/silent-blocks/silent-blocks.service.ts +++ b/src/silent-blocks/silent-blocks.service.ts @@ -15,9 +15,8 @@ export class SilentBlocksService { */ private encodeSilentBlock(transactions: Transaction[]): Buffer { const silentBlockType = SILENT_PAYMENT_BLOCK_TYPE; - const transactionBuffers: Buffer[] = []; - transactions.forEach((tx) => { + const transactionBuffers = transactions.map((tx) => { const txidBuffer = Buffer.from(tx.id, 'hex'); const outputsCount = tx.outputs.length; @@ -42,14 +41,12 @@ export class SilentBlocksService { const scanTweakBuffer = Buffer.from(tx.scanTweak, 'hex'); - const transactionBuffer = Buffer.concat([ + return Buffer.concat([ txidBuffer, outputsCountBuffer, outputsBuffer, scanTweakBuffer, ]); - - transactionBuffers.push(transactionBuffer); }); const transactionsCountBuffer = this.encodeVarInt( @@ -78,41 +75,50 @@ export class SilentBlocksService { const silentBlockType = buffer.readUInt8(offset); offset += 1; - // Read the number of transactions - const transactionsCount = this.decodeVarInt(buffer, offset); - offset += 1; + if (silentBlockType !== SILENT_PAYMENT_BLOCK_TYPE) { + throw new Error('Invalid silent block type'); + } + + // Read the number of transactions (Bitcoin varInt format) + const { value: transactionsCount, bytesRead: txCountBytesRead } = + this.decodeVarInt(buffer, offset); + offset += txCountBytesRead; // Process each transaction - for (let i = 0; i < transactionsCount; i++) { - // Read the txid + for (let i = 0; i < Number(transactionsCount); i++) { + // Read the txid (32 bytes) const txid = buffer.subarray(offset, offset + 32).toString('hex'); offset += 32; - // Read the number of outputs - const outputsCount = this.decodeVarInt(buffer, offset); - offset += 1; + // Read the number of outputs (Bitcoin varInt format) + const { value: outputsCount, bytesRead: outCountBytesRead } = + this.decodeVarInt(buffer, offset); + offset += outCountBytesRead; // Read the outputs const outputs = []; - for (let j = 0; j < outputsCount; j++) { - const value = buffer.readBigUInt64BE(offset); // 8-byte value + for (let j = 0; j < Number(outputsCount); j++) { + // Read the value (8-byte big-endian) + const value = buffer.readBigUInt64BE(offset); offset += 8; + // Read the pubKey (32-byte pubkey) const pubKey = buffer .subarray(offset, offset + 32) - .toString('hex'); // 32-byte pubkey + .toString('hex'); offset += 32; - const vout = buffer.readUInt32BE(offset); // 4-byte vout + // Read the vout (4-byte big-endian) + const vout = buffer.readUInt32BE(offset); offset += 4; outputs.push({ value: Number(value), pubKey, vout }); } - // Read the scanTweak + // Read the scanTweak (33 bytes) const scanTweak = buffer .subarray(offset, offset + 33) - .toString('hex'); // 33-byte scan tweak + .toString('hex'); offset += 33; // Create the transaction object @@ -121,7 +127,7 @@ export class SilentBlocksService { outputs, scanTweak, - // These are not included in the block design, so just use dummy values + // These fields are not in the block, so use dummy values blockHeight: 0, blockHash: '', isSpent: false, @@ -134,24 +140,57 @@ export class SilentBlocksService { } /** - * Encodes a variable integer (varInt) into a Buffer. + * Encodes a variable integer (varInt) into a Buffer following Bitcoin's encoding scheme. * @param value - The integer value to encode. * @returns A Buffer containing the encoded varInt. */ private encodeVarInt(value: number): Buffer { - const buffer = Buffer.alloc(1); - buffer.writeUInt8(value); - return buffer; + if (value < 0xfd) { + return Buffer.from([value]); + } else if (value <= 0xffff) { + const buffer = Buffer.alloc(3); + buffer.writeUInt8(0xfd, 0); + buffer.writeUInt16LE(value, 1); + return buffer; + } else if (value <= 0xffffffff) { + const buffer = Buffer.alloc(5); + buffer.writeUInt8(0xfe, 0); + buffer.writeUInt32LE(value, 1); + return buffer; + } else { + const buffer = Buffer.alloc(9); + buffer.writeUInt8(0xff, 0); + buffer.writeBigUInt64LE(BigInt(value), 1); + return buffer; + } } /** - * Decodes a variable integer (varInt) from a Buffer. + * Decodes a variable integer (varInt) from a Buffer following Bitcoin's encoding scheme. * @param buffer - The buffer containing the encoded varInt. * @param offset - The offset in the buffer where the varInt starts. - * @returns The decoded integer value. + * @returns The decoded integer value and the number of bytes read. */ - private decodeVarInt(buffer: Buffer, offset: number): number { - return buffer.readUInt8(offset); + private decodeVarInt( + buffer: Buffer, + offset: number, + ): { value: number | bigint; bytesRead: number } { + const firstByte = buffer.readUInt8(offset); + + // Check for invalid prefix bytes + if (firstByte < 0x00 || firstByte > 0xff) { + throw new Error('Invalid VarInt prefix byte'); + } + + if (firstByte < 0xfd) { + return { value: firstByte, bytesRead: 1 }; + } else if (firstByte === 0xfd) { + return { value: buffer.readUInt16LE(offset + 1), bytesRead: 3 }; + } else if (firstByte === 0xfe) { + return { value: buffer.readUInt32LE(offset + 1), bytesRead: 5 }; + } else { + return { value: buffer.readBigUInt64LE(offset + 1), bytesRead: 9 }; + } } /**