From 9f97c57b4ead983a7fcf5bc8614bd0c7dd61333f Mon Sep 17 00:00:00 2001 From: Zachary Belford Date: Tue, 5 Nov 2024 22:37:14 -0800 Subject: [PATCH 01/12] feat: add support for secure account type --- .node-version | 1 + src/shardeum/calculateAccountHash.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .node-version diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..3876fd4 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +18.16.1 diff --git a/src/shardeum/calculateAccountHash.ts b/src/shardeum/calculateAccountHash.ts index d4f6b20..a61163c 100644 --- a/src/shardeum/calculateAccountHash.ts +++ b/src/shardeum/calculateAccountHash.ts @@ -32,7 +32,7 @@ export const accountSpecificHash = (account: any): string => { account.accountType === AccountType.UnstakeReceipt || account.accountType === AccountType.InternalTxReceipt || account.accountType === AccountType.DevAccount || - account.accountType === AccountType.SecureAccount + account.accountType === AccountType.SecureAccount // might need to parse bigints before hashing... ) { account.hash = crypto.hashObj(account) return account.hash From 76ac0e51fcfd5fa4eceb001993c63d13ed524d57 Mon Sep 17 00:00:00 2001 From: Zachary Belford Date: Thu, 7 Nov 2024 08:29:36 -0800 Subject: [PATCH 02/12] fix: remove .node-version file --- .node-version | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .node-version diff --git a/.node-version b/.node-version deleted file mode 100644 index 3876fd4..0000000 --- a/.node-version +++ /dev/null @@ -1 +0,0 @@ -18.16.1 From 496ab5b669925b086b740b03ab68c8aa67087889 Mon Sep 17 00:00:00 2001 From: Zachary Belford Date: Thu, 7 Nov 2024 08:30:29 -0800 Subject: [PATCH 03/12] Update src/shardeum/calculateAccountHash.ts --- src/shardeum/calculateAccountHash.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shardeum/calculateAccountHash.ts b/src/shardeum/calculateAccountHash.ts index a61163c..d4f6b20 100644 --- a/src/shardeum/calculateAccountHash.ts +++ b/src/shardeum/calculateAccountHash.ts @@ -32,7 +32,7 @@ export const accountSpecificHash = (account: any): string => { account.accountType === AccountType.UnstakeReceipt || account.accountType === AccountType.InternalTxReceipt || account.accountType === AccountType.DevAccount || - account.accountType === AccountType.SecureAccount // might need to parse bigints before hashing... + account.accountType === AccountType.SecureAccount ) { account.hash = crypto.hashObj(account) return account.hash From 8ea8d8facaa444ff10b6b92fd348eb18a79299d6 Mon Sep 17 00:00:00 2001 From: Zachary Belford Date: Fri, 22 Nov 2024 13:51:18 -0800 Subject: [PATCH 04/12] feat: tickets --- archiver-config.json | 7 +- jest.config.js | 12 +- package.json | 7 +- src/API.ts | 27 +++ src/Config.ts | 8 +- src/routes/tickets.ts | 268 +++++++++++++++++++++++++++ src/schemas/ticketSchema.ts | 65 +++++++ src/server.ts | 4 +- static/tickets.json | 16 ++ test/tsconfig.json | 11 ++ test/unit/src/routes/tickets.test.ts | 118 ++++++++++++ 11 files changed, 534 insertions(+), 9 deletions(-) create mode 100644 src/routes/tickets.ts create mode 100644 src/schemas/ticketSchema.ts create mode 100644 static/tickets.json create mode 100644 test/tsconfig.json create mode 100644 test/unit/src/routes/tickets.test.ts diff --git a/archiver-config.json b/archiver-config.json index 7fafd0a..a02dcc6 100644 --- a/archiver-config.json +++ b/archiver-config.json @@ -56,6 +56,9 @@ "publicKey": "aec5d2b663869d9c22ba99d8de76f3bff0f54fa5e39d2899ec1f3f4543422ec7" } ], - "ARCHIVER_MODE": "release", - "DevPublicKey": "" + "ARCHIVER_MODE": "debug", + "DevPublicKey": "", + "allowedTicketSigners": { + "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47": 3 + } } \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 21fe299..9ad9e27 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,6 +9,14 @@ module.exports = { "**/?(*.)+(spec|test).+(ts|tsx|js)" ], transform: { - "^.+\\.(ts|tsx)$": "ts-jest" + "^.+\\.(ts|tsx)$": ["ts-jest", { + tsconfig: "test/tsconfig.json" + }] }, - }; \ No newline at end of file + moduleDirectories: ["node_modules", "src"], + globals: { + 'ts-jest': { + isolatedModules: true + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 6029f94..9e546e0 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,9 @@ "pretest": "npm run compile", "update-docker": "docker build -t registry.gitlab.com/shardus/archive/archive-server:dev3 . && docker push registry.gitlab.com/shardus/archive/archive-server:dev3", "update-docker-dev": "docker build -t registry.gitlab.com/shardus/archive/archive-server:dev . && docker push registry.gitlab.com/shardus/archive/archive-server:dev", - "update-schemas": "typescript-json-schema tsconfig.json NewData -o src/Data/schemas/NewData.json & typescript-json-schema tsconfig.json DataResponse -o src/Data/schemas/DataResponse.json" + "update-schemas": "typescript-json-schema tsconfig.json NewData -o src/Data/schemas/NewData.json & typescript-json-schema tsconfig.json DataResponse -o src/Data/schemas/DataResponse.json", + "build": "tsc && npm run copy-static", + "copy-static": "cp -r static dist/" }, "repository": { "type": "git", @@ -84,6 +86,7 @@ "@shardus/crypto-utils": "git+https://github.com/shardeum/lib-crypto-utils#v4.1.4", "@shardus/types": "git+https://github.com/shardeum/lib-types#v1.2.21", "deepmerge": "^4.2.2", + "ethers": "^6.13.4", "fastify": "4.12.0", "log4js": "^6.3.0", "log4js-extend": "^0.2.1", @@ -100,4 +103,4 @@ "overrides": { "axios": "1.6.1" } -} \ No newline at end of file +} diff --git a/src/API.ts b/src/API.ts index 73f5da7..000f177 100644 --- a/src/API.ts +++ b/src/API.ts @@ -35,6 +35,9 @@ import { failureReceiptCount, } from './primary-process' import * as ServiceQueue from './ServiceQueue' +import { readFileSync } from 'fs' +import { join } from 'path' +import ticketRoutes from './routes/tickets' const { version } = require('../package.json') // eslint-disable-line @typescript-eslint/no-var-requires const TXID_LENGTH = 64 @@ -1261,6 +1264,30 @@ export function registerRoutes(server: FastifyInstance { + if (reachabilityAllowed) { + try { + const jsonData = readFileSync(join(process.cwd(), config.STATIC_FILES.TICKETS_JSON), 'utf8') + const tickets = JSON.parse(jsonData) + const silverTicket = tickets.find(t => t.type === 'silver') + + if (!silverTicket) { + reply.code(404).send({ error: 'Silver ticket whitelist not found' }) + return + } + + reply.send(silverTicket) + } catch (err) { + reply.code(500).send({ error: 'Failed to read silver ticket whitelist' }) + } + } else { + request.raw.socket.destroy() + } + }) + + // Register ticket routes + server.register(ticketRoutes, { prefix: '/tickets' }) } export const validateRequestData = ( diff --git a/src/Config.ts b/src/Config.ts index 6faf7fe..7c05721 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -102,6 +102,12 @@ export interface Config { disableOffloadReceipt: boolean // To disable offloading of receipts globally disableOffloadReceiptForGlobalModification: boolean // To disable offloading of receipts for global modifications receipts restoreNGTsFromSnapshot: boolean // To restore NGTs from snapshot + STATIC_FILES: { + TICKETS_JSON: string + } + allowedTicketSigners: { + [pubkey: string]: number + } } let config: Config = { @@ -131,7 +137,7 @@ let config: Config = { save: true, interval: 1, }, - ARCHIVER_MODE: 'release', // 'debug'/'release' + ARCHIVER_MODE: 'debug', // 'debug'/'release' DevPublicKey: '', dataLogWrite: true, dataLogWriter: { diff --git a/src/routes/tickets.ts b/src/routes/tickets.ts new file mode 100644 index 0000000..708e2bc --- /dev/null +++ b/src/routes/tickets.ts @@ -0,0 +1,268 @@ +import { FastifyPluginCallback } from 'fastify' +import { readFileSync } from 'fs' +import { join } from 'path' +import { config } from '../Config' +import * as Logger from '../Logger' +import { ethers } from 'ethers' +import { Utils } from '@shardus/types' +import * as Ajv from 'ajv' +import { ticketSchema, type Ticket, type Sign } from '../schemas/ticketSchema' + +interface TicketData { + address: string; +} + +enum DevSecurityLevel { + NONE = 0, + LOW = 1, + MEDIUM = 2, + HIGH = 3 +} + +interface VerificationError { + type: string; + message: string; + validSignatures: number; +} + +function verifyMultiSigs( + rawPayload: object, + sigs: Sign[], + allowedPubkeys: { [pubkey: string]: DevSecurityLevel }, + minSigRequired: number, + requiredSecurityLevel: DevSecurityLevel +): { isValid: boolean; validCount: number } { + if (sigs.length < minSigRequired) return { isValid: false, validCount: 0 }; + + if (sigs.length > Object.keys(allowedPubkeys).length) return { isValid: false, validCount: 0 }; + + let validSigs = 0; + const payload_hash = ethers.keccak256(ethers.toUtf8Bytes(Utils.safeStringify(rawPayload))); + const seen = new Set(); + + for (let i = 0; i < sigs.length; i++) { + if ( + !seen.has(sigs[i].owner) && + allowedPubkeys[sigs[i].owner] && + allowedPubkeys[sigs[i].owner] >= requiredSecurityLevel && + ethers.verifyMessage(payload_hash, sigs[i].sig) === sigs[i].owner + ) { + validSigs++; + seen.add(sigs[i].owner); + } + } + + return { + isValid: validSigs >= minSigRequired, + validCount: validSigs + }; +} + +function verifyTickets( + tickets: Ticket[], + allowedTicketSigners?: { [pubkey: string]: DevSecurityLevel }, + configPath: string = join(process.cwd(), 'archiver-config.json') +): { isValid: boolean; errors: VerificationError[] } { + // First validate against schema + if (!validateTicketSchema(tickets)) { + return { + isValid: false, + errors: [{ + type: 'schema', + message: `Schema validation failed: ${ajv.errorsText(validateTicketSchema.errors)}`, + validSignatures: 0 + }] + }; + } + + // Load config if allowedTicketSigners not provided + if (!allowedTicketSigners) { + const config = JSON.parse(readFileSync(configPath, 'utf8')); + allowedTicketSigners = config.allowedTicketSigners || {}; + } + + const minSigRequired = 5; + const requiredSecurityLevel = DevSecurityLevel.HIGH; + const errors: VerificationError[] = []; + + // Continue with signature verification + for (const ticket of tickets) { + const { data, sign, type } = ticket; + + const messageObj = { + data, + type + }; + + const verificationResult = verifyMultiSigs( + messageObj, + sign, + allowedTicketSigners, + minSigRequired, + requiredSecurityLevel + ); + + if (!verificationResult.isValid) { + errors.push({ + type, + message: `Invalid signatures for ticket type ${type}. ` + + `Found ${verificationResult.validCount} valid signatures, ` + + `required ${minSigRequired} with security level HIGH`, + validSignatures: verificationResult.validCount + }); + } + } + + return { + isValid: errors.length === 0, + errors + }; +} + +// Initialize Ajv with strict mode +const ajv = new Ajv({ + allErrors: true +}) + +// Add the schema +const validateTicketSchema = ajv.compile(ticketSchema) + +export const ticketsRouter: FastifyPluginCallback = function (fastify, opts, done) { + // GET / - Get all tickets + fastify.get('/', (_request, reply) => { + try { + const filePath = join(__dirname, '..', '..', config.STATIC_FILES.TICKETS_JSON) + let jsonData: string + + try { + jsonData = readFileSync(filePath, 'utf8') + } catch (err) { + Logger.mainLogger.error('Failed to read tickets file:', err) + reply.code(500).send({ + error: 'Unable to access tickets configuration', + code: 'TICKETS_FILE_NOT_ACCESSIBLE' + }) + return + } + + try { + const tickets = JSON.parse(jsonData) + if (!Array.isArray(tickets)) { + Logger.mainLogger.error('Tickets data is not an array') + reply.code(500).send({ + error: 'Invalid tickets configuration format', + code: 'INVALID_TICKETS_FORMAT' + }) + return + } + + // Verify tickets before returning + const verificationResult = verifyTickets(tickets, config.allowedTicketSigners || {}); + if (!verificationResult.isValid) { + Logger.mainLogger.error('Ticket verification failed:', verificationResult.errors) + reply.code(400).send({ + error: 'Ticket verification failed', + code: 'INVALID_TICKET_SIGNATURES', + details: verificationResult.errors + }) + return + } + + reply.send(tickets) + } catch (err) { + Logger.mainLogger.error('Failed to parse tickets JSON:', err) + reply.code(500).send({ + error: 'Invalid tickets configuration data', + code: 'INVALID_TICKETS_DATA' + }) + } + } catch (err) { + Logger.mainLogger.error('Unexpected error in GET /tickets:', err) + reply.code(500).send({ + error: 'Internal server error', + code: 'INTERNAL_SERVER_ERROR' + }) + } + }) + + // GET /:type - Get tickets by type + fastify.get('/:type', (request, reply) => { + try { + const { type } = request.params as { type: string } + + if (!type || typeof type !== 'string') { + reply.code(400).send({ + error: 'Invalid ticket type parameter', + code: 'INVALID_TICKET_TYPE' + }) + return + } + + const filePath = join(__dirname, '..', '..', config.STATIC_FILES.TICKETS_JSON) + let jsonData: string + + try { + jsonData = readFileSync(filePath, 'utf8') + } catch (err) { + Logger.mainLogger.error('Failed to read tickets file:', err) + reply.code(500).send({ + error: 'Unable to access tickets configuration', + code: 'TICKETS_FILE_NOT_ACCESSIBLE' + }) + return + } + + try { + const tickets = JSON.parse(jsonData) + if (!Array.isArray(tickets)) { + Logger.mainLogger.error('Tickets data is not an array') + reply.code(500).send({ + error: 'Invalid tickets configuration format', + code: 'INVALID_TICKETS_FORMAT' + }) + return + } + + const ticket = tickets.find((t: { type: string }) => t.type === type) + + if (!ticket) { + reply.code(404).send({ + error: `No ticket found with type: ${type}`, + code: 'TICKET_NOT_FOUND' + }) + return + } + + // Verify single ticket before returning + const verificationResult = verifyTickets([ticket], config.allowedTicketSigners || {}); + if (!verificationResult.isValid) { + Logger.mainLogger.error('Ticket verification failed:', verificationResult.errors) + reply.code(400).send({ + error: 'Ticket verification failed', + code: 'INVALID_TICKET_SIGNATURES', + details: verificationResult.errors + }) + return + } + + reply.send(ticket) + } catch (err) { + Logger.mainLogger.error('Failed to parse tickets JSON:', err) + reply.code(500).send({ + error: 'Invalid tickets configuration data', + code: 'INVALID_TICKETS_DATA' + }) + } + } catch (err) { + Logger.mainLogger.error('Unexpected error in GET /tickets/:type:', err) + reply.code(500).send({ + error: 'Internal server error', + code: 'INTERNAL_SERVER_ERROR' + }) + } + }) + + done() +} + +export default ticketsRouter \ No newline at end of file diff --git a/src/schemas/ticketSchema.ts b/src/schemas/ticketSchema.ts new file mode 100644 index 0000000..49c5397 --- /dev/null +++ b/src/schemas/ticketSchema.ts @@ -0,0 +1,65 @@ +type TicketData = { + address: string; +} + +type Sign = { + owner: string; + sig: string; +} + +type Ticket = { + data: TicketData[]; + sign: Sign[]; + type: string; +} + +export const ticketSchema = { + type: 'array', + items: { + type: 'object', + required: ['data', 'sign', 'type'], + properties: { + data: { + type: 'array', + items: { + type: 'object', + required: ['address'], + properties: { + address: { + type: 'string', + pattern: '^0x[a-fA-F0-9]{40}$' // Ethereum address format + } + }, + additionalProperties: false + }, + minItems: 1 + }, + sign: { + type: 'array', + items: { + type: 'object', + required: ['owner', 'sig'], + properties: { + owner: { + type: 'string', + pattern: '^0x[a-fA-F0-9]{40}$' // Ethereum address format + }, + sig: { + type: 'string', + pattern: '^0x[a-fA-F0-9]{130}$' // Ethereum signature format (65 bytes) + } + }, + additionalProperties: false + }, + minItems: 1 + }, + type: { + type: 'string', + enum: ['silver'] // Only silver tickets for now + } + }, + additionalProperties: false + } +} as const + +export type { TicketData, Sign, Ticket } \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 82a35f8..4e678ba 100644 --- a/src/server.ts +++ b/src/server.ts @@ -475,12 +475,12 @@ async function startServer(): Promise { host: '0.0.0.0', }, (err) => { - Logger.mainLogger.debug('Listening', config.ARCHIVER_PORT) if (err) { server.log.error(err) process.exit(1) } - Logger.mainLogger.debug('Archive-server has started.') + console.log(`Worker ${process.pid}: Archive-server is listening on http://0.0.0.0:${config.ARCHIVER_PORT}`) + Logger.mainLogger.info(`Worker ${process.pid}: Archive-server is listening on http://0.0.0.0:${config.ARCHIVER_PORT}`) State.setActive() Collector.scheduleMissingTxsDataQuery() setupWorkerProcesses(cluster) diff --git a/static/tickets.json b/static/tickets.json new file mode 100644 index 0000000..0424c79 --- /dev/null +++ b/static/tickets.json @@ -0,0 +1,16 @@ +[ + { + "data": [ + { + "address": "0x37a9FCf5628B1C198A01C9eDaB0BF5C4d453E928" + } + ], + "sign": [ + { + "owner": "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47", + "sig": "0x5f1aad2caa2cca1f725715ed050b1928527f0c4eb815fb282fad113ca866a63568d9c003b5310e16de67103521bf284fda10728b4fffc66055c55fde5934438d1b" + } + ], + "type": "silver" + } +] \ No newline at end of file diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..00ad60b --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "../", + "types": ["node", "jest"] + }, + "include": [ + "../src/**/*", + "./**/*" + ] +} \ No newline at end of file diff --git a/test/unit/src/routes/tickets.test.ts b/test/unit/src/routes/tickets.test.ts new file mode 100644 index 0000000..28f9116 --- /dev/null +++ b/test/unit/src/routes/tickets.test.ts @@ -0,0 +1,118 @@ +import { FastifyInstance } from 'fastify' +import { readFileSync } from 'fs' +import { join } from 'path' +import { config } from '../../../../src/Config' +import { ticketsRouter } from '../../../../src/routes/tickets' + +// Mock Logger +jest.mock('../../../../src/Logger', () => ({ + mainLogger: { + debug: jest.fn(), + error: jest.fn(), + info: jest.fn() + } +})) + +describe('Ticket Routes', () => { + // Read the actual tickets file once at the start + const tickets = JSON.parse( + readFileSync(join(process.cwd(), config.STATIC_FILES.TICKETS_JSON), 'utf8') + ) + + describe('GET /', () => { + it('should return all tickets', async () => { + const mockReply = { + send: jest.fn(), + code: jest.fn().mockReturnThis() + } + + const mockFastify = { + get: jest.fn() + } as unknown as FastifyInstance + + // Call the plugin with the required arguments + ticketsRouter(mockFastify, {}, (err) => { + if (err) throw err + }) + + // Get the handler that was registered + const handler = mockFastify.get.mock.calls[0][1] + + // Call the handler directly + await handler({}, mockReply) + + expect(mockReply.send).toHaveBeenCalledWith(tickets) + expect(mockFastify.get).toHaveBeenCalledWith('/', expect.any(Function)) + }) + }) + + describe('GET /:type', () => { + it('should return silver ticket when requested', async () => { + const mockReply = { + send: jest.fn(), + code: jest.fn().mockReturnThis() + } + + const mockFastify = { + get: jest.fn() + } as unknown as FastifyInstance + + ticketsRouter(mockFastify, {}, (err) => { + if (err) throw err + }) + + const handler = mockFastify.get.mock.calls[1][1] + await handler({ params: { type: 'silver' } }, mockReply) + + expect(mockReply.send).toHaveBeenCalledWith(tickets[0]) + }) + + it('should return 404 for non-existent ticket type', async () => { + const mockReply = { + send: jest.fn(), + code: jest.fn().mockReturnThis() + } + + const mockFastify = { + get: jest.fn() + } as unknown as FastifyInstance + + ticketsRouter(mockFastify, {}, (err) => { + if (err) throw err + }) + + const handler = mockFastify.get.mock.calls[1][1] + await handler({ params: { type: 'gold' } }, mockReply) + + expect(mockReply.code).toHaveBeenCalledWith(404) + expect(mockReply.send).toHaveBeenCalledWith({ + error: 'No ticket found with type: gold', + code: 'TICKET_NOT_FOUND' + }) + }) + + it('should handle invalid type parameter', async () => { + const mockReply = { + send: jest.fn(), + code: jest.fn().mockReturnThis() + } + + const mockFastify = { + get: jest.fn() + } as unknown as FastifyInstance + + ticketsRouter(mockFastify, {}, (err) => { + if (err) throw err + }) + + const handler = mockFastify.get.mock.calls[1][1] + await handler({ params: { type: undefined } }, mockReply) + + expect(mockReply.code).toHaveBeenCalledWith(400) + expect(mockReply.send).toHaveBeenCalledWith({ + error: 'Invalid ticket type parameter', + code: 'INVALID_TICKET_TYPE' + }) + }) + }) +}) \ No newline at end of file From 6bff93c25264acd64775b7461c50badacfea03d1 Mon Sep 17 00:00:00 2001 From: Zachary Belford Date: Fri, 22 Nov 2024 15:17:16 -0800 Subject: [PATCH 05/12] fix: refactor and add tests --- archiver-config.json | 4 +- jest.config.js | 3 +- package-lock.json | 103 +++++ src/API.ts | 21 - src/Config.ts | 37 +- src/routes/tickets.ts | 391 ++++++++---------- src/schemas/ticketSchema.ts | 6 +- src/services/ticketVerification.ts | 121 ++++++ src/types/errors.ts | 20 + src/types/security.ts | 6 + src/types/tickets.ts | 3 + test/unit/src/routes/tickets.test.ts | 313 +++++++++----- .../src/services/ticketVerification.test.ts | 203 +++++++++ 13 files changed, 870 insertions(+), 361 deletions(-) create mode 100644 src/services/ticketVerification.ts create mode 100644 src/types/errors.ts create mode 100644 src/types/security.ts create mode 100644 src/types/tickets.ts create mode 100644 test/unit/src/services/ticketVerification.test.ts diff --git a/archiver-config.json b/archiver-config.json index a02dcc6..c97dce3 100644 --- a/archiver-config.json +++ b/archiver-config.json @@ -60,5 +60,7 @@ "DevPublicKey": "", "allowedTicketSigners": { "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47": 3 - } + }, + "minSigRequired": 5, + "requiredSecurityLevel": 3 } \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 9ad9e27..15b8c54 100644 --- a/jest.config.js +++ b/jest.config.js @@ -18,5 +18,6 @@ module.exports = { 'ts-jest': { isolatedModules: true } - } + }, + timers: 'fake' } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a15abcb..3a5085f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@shardus/crypto-utils": "git+https://github.com/shardeum/lib-crypto-utils#v4.1.4", "@shardus/types": "git+https://github.com/shardeum/lib-types#v1.2.21", "deepmerge": "^4.2.2", + "ethers": "^6.13.4", "fastify": "4.12.0", "log4js": "^6.3.0", "log4js-extend": "^0.2.1", @@ -64,6 +65,11 @@ "node": "18.19.1" } }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -1456,6 +1462,28 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2297,6 +2325,11 @@ "node": ">=0.4.0" } }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==" + }, "node_modules/after": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", @@ -5366,6 +5399,71 @@ "node": ">=0.10.0" } }, + "node_modules/ethers": { + "version": "6.13.4", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.4.tgz", + "integrity": "sha512-21YtnZVg4/zKkCQPjrDj38B1r4nQvTZLopUGMLQ1ePU2zV/joCfDC3t3iKQjWRzjjjbzR+mdAIoikeBRNkdllA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "node_modules/ethers/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -12754,6 +12852,11 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", "integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==" }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/socks": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", diff --git a/src/API.ts b/src/API.ts index 000f177..3f94b32 100644 --- a/src/API.ts +++ b/src/API.ts @@ -1265,27 +1265,6 @@ export function registerRoutes(server: FastifyInstance { - if (reachabilityAllowed) { - try { - const jsonData = readFileSync(join(process.cwd(), config.STATIC_FILES.TICKETS_JSON), 'utf8') - const tickets = JSON.parse(jsonData) - const silverTicket = tickets.find(t => t.type === 'silver') - - if (!silverTicket) { - reply.code(404).send({ error: 'Silver ticket whitelist not found' }) - return - } - - reply.send(silverTicket) - } catch (err) { - reply.code(500).send({ error: 'Failed to read silver ticket whitelist' }) - } - } else { - request.raw.socket.destroy() - } - }) - // Register ticket routes server.register(ticketRoutes, { prefix: '/tickets' }) } diff --git a/src/Config.ts b/src/Config.ts index 7c05721..8f10e9e 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -102,9 +102,6 @@ export interface Config { disableOffloadReceipt: boolean // To disable offloading of receipts globally disableOffloadReceiptForGlobalModification: boolean // To disable offloading of receipts for global modifications receipts restoreNGTsFromSnapshot: boolean // To restore NGTs from snapshot - STATIC_FILES: { - TICKETS_JSON: string - } allowedTicketSigners: { [pubkey: string]: number } @@ -201,6 +198,40 @@ let config: Config = { disableOffloadReceipt: false, disableOffloadReceiptForGlobalModification: true, restoreNGTsFromSnapshot: false, + allowedTicketSigners: { // copied from shardeum config for now... + /* prettier-ignore */ '0x002D3a2BfE09E3E29b6d38d58CaaD16EEe4C9BC5': 5, + /* prettier-ignore */ '0x0a0844DA5e01E391d12999ca859Da8a897D5979A': 5, + /* prettier-ignore */ '0x390878B18DeBe2A9f0d5c0252a109c84243D3beb': 5, + /* prettier-ignore */ '0x32B6f2C027D4c9D99Ca07d047D17987390a5EB39': 5, + /* prettier-ignore */ '0x80aF8E195B56aCC3b4ec8e2C99EC38957258635a': 5, + /* prettier-ignore */ '0x7Efbb31431ac7C405E8eEba99531fF1254fCA3B6': 5, + /* prettier-ignore */ '0xCc74bf387F6C102b5a7F828796C57A6D2D19Cb00': 5, + /* prettier-ignore */ '0x4ed5C053BF2dA5F694b322EA93dce949F3276B85': 5, + /* prettier-ignore */ '0xd31aBC7497aD8bC9fe8555C9eDe45DFd7FB3Bf6F': 5, + /* prettier-ignore */ '0xe7e4cc292b424C6D50d16F1Bb5BAB2032c486980': 5, + /* prettier-ignore */ '0xD815DA50966c19261B34Ffa3bE50A30A67D97456': 5, + /* prettier-ignore */ '0xE856B2365641eba73Bc430AAC1E8F930dA513D9D': 5, + /* prettier-ignore */ '0x8282F755e784414697421D4b59232E5d194e2262': 5, + /* prettier-ignore */ '0x353Ad64Df4fAe5EffF717A1c41BE6dEBee543129': 5, + /* prettier-ignore */ '0x9Ce1C3c114538c625aA2488b97fEb3723fdBB07B': 5, + /* prettier-ignore */ '0x6A83e4e4eB0A2c8f562db6BB64b02a9A6237B314': 5, + /* prettier-ignore */ '0x92E375E0c76CaE76D9DfBab17EE7B3B4EE407715': 5, + /* prettier-ignore */ '0xBD79B430CA932e2D89bb77ACaE7367a07471c2eA': 5, + /* prettier-ignore */ '0xEbe173a837Bc30BFEF6E13C9988a4771a4D83275': 5, + /* prettier-ignore */ '0xfF2b584A947182c55BBc039BEAB78BC201D3AdDe': 5, + /* prettier-ignore */ '0xCeA068d8DCB4B4020D30a9950C00cF8408611F67': 5, + /* prettier-ignore */ '0x52F8d3DaA7b5FF25ca2bF7417E059aFe0bD5fB0E': 5, + /* prettier-ignore */ '0x0341996A92193d8B7d80C4774fA2eff889e4b427': 5, + /* prettier-ignore */ '0xF82BDA6Ef512e4219C6DCEea896E50e8180a5bff': 5, + /* prettier-ignore */ '0xA04A1B214a2537139fE59488820D4dA06516933f': 5, + /* prettier-ignore */ '0x550817e7B91244BBeFE2AD621ccD555A16B00405': 5, + /* prettier-ignore */ '0x84C55a4bFfff1ADadb9C46e2B60979F519dAf874': 5, + /* prettier-ignore */ '0x4563303BCE96D3f8d9C7fB94b36dfFC9d831871d': 5, + /* prettier-ignore */ '0xdA058F9c7Ce86C1D21DD5DBDeBad5ab5c785520a': 5, + /* prettier-ignore */ '0x0950C3Ecc7d1c4dd093C9652F335F9391d83Ee99': 5, + /* prettier-ignore */ '0x05b67C84bf88d93E795039d5bBA9CeC9Dcd93087': 5, + /* prettier-ignore */ '0x891DF765C855E9848A18Ed18984B9f57cb3a4d47': 5, // zb added this one for testing + } } // Override default config params from config file, env vars, and cli args export async function overrideDefaultConfig(file: string): Promise { diff --git a/src/routes/tickets.ts b/src/routes/tickets.ts index 708e2bc..e7dd6c1 100644 --- a/src/routes/tickets.ts +++ b/src/routes/tickets.ts @@ -3,266 +3,223 @@ import { readFileSync } from 'fs' import { join } from 'path' import { config } from '../Config' import * as Logger from '../Logger' -import { ethers } from 'ethers' -import { Utils } from '@shardus/types' import * as Ajv from 'ajv' -import { ticketSchema, type Ticket, type Sign } from '../schemas/ticketSchema' +import { ticketSchema, type Ticket } from '../schemas/ticketSchema' +import { verifyTickets, VerificationConfig, VerificationError } from '../services/ticketVerification' +import { ApiError, ErrorCodes } from '../types/errors' +import { DevSecurityLevel } from '../types/security' -interface TicketData { - address: string; -} - -enum DevSecurityLevel { - NONE = 0, - LOW = 1, - MEDIUM = 2, - HIGH = 3 -} +const ajv = new Ajv({ allErrors: true }) +const validateTicketSchema = ajv.compile(ticketSchema) +const ticketFilePath = join(__dirname, '..', '..', 'static', 'tickets.json') -interface VerificationError { - type: string; - message: string; - validSignatures: number; -} +// Export for testing +export let ticketCache: TicketCache | null = null; -function verifyMultiSigs( - rawPayload: object, - sigs: Sign[], - allowedPubkeys: { [pubkey: string]: DevSecurityLevel }, - minSigRequired: number, - requiredSecurityLevel: DevSecurityLevel -): { isValid: boolean; validCount: number } { - if (sigs.length < minSigRequired) return { isValid: false, validCount: 0 }; +const CACHE_TTL = 60 * 1000; // 1 minute in milliseconds - if (sigs.length > Object.keys(allowedPubkeys).length) return { isValid: false, validCount: 0 }; +interface TicketCache { + tickets: Ticket[]; + lastRead: number; +} - let validSigs = 0; - const payload_hash = ethers.keccak256(ethers.toUtf8Bytes(Utils.safeStringify(rawPayload))); - const seen = new Set(); +const verificationConfig: VerificationConfig = { + allowedTicketSigners: config.allowedTicketSigners, + minSigRequired: process.env.NODE_ENV === 'production' ? 5 : 1, + requiredSecurityLevel: DevSecurityLevel.HIGH +}; - for (let i = 0; i < sigs.length; i++) { - if ( - !seen.has(sigs[i].owner) && - allowedPubkeys[sigs[i].owner] && - allowedPubkeys[sigs[i].owner] >= requiredSecurityLevel && - ethers.verifyMessage(payload_hash, sigs[i].sig) === sigs[i].owner - ) { - validSigs++; - seen.add(sigs[i].owner); - } +function createApiError(code: keyof typeof ErrorCodes, message: string, details?: unknown): ApiError { + let statusCode = 500; + + // Map error codes to appropriate HTTP status codes + if (code.startsWith('INVALID')) { + statusCode = 400; + } else if (code === 'TICKET_NOT_FOUND') { + statusCode = 404; } - return { - isValid: validSigs >= minSigRequired, - validCount: validSigs + return { + statusCode, + response: { + error: message, + code: ErrorCodes[code], + ...(details && { details }) + } }; } -function verifyTickets( - tickets: Ticket[], - allowedTicketSigners?: { [pubkey: string]: DevSecurityLevel }, - configPath: string = join(process.cwd(), 'archiver-config.json') -): { isValid: boolean; errors: VerificationError[] } { - // First validate against schema - if (!validateTicketSchema(tickets)) { - return { - isValid: false, - errors: [{ - type: 'schema', - message: `Schema validation failed: ${ajv.errorsText(validateTicketSchema.errors)}`, - validSignatures: 0 - }] - }; +function handleFileError(err: Error): ApiError { + Logger.mainLogger.error('Failed to read tickets file:', err); + return createApiError( + 'TICKETS_FILE_NOT_ACCESSIBLE', + `Unable to access tickets configuration: ${ticketFilePath}` + ); +} + +function handleJsonParseError(err: Error): ApiError { + Logger.mainLogger.error('Failed to parse tickets JSON:', err); + return createApiError( + 'INVALID_TICKETS_DATA', + 'Invalid tickets configuration data' + ); +} + +function handleVerificationError(errors: VerificationError[]): ApiError { + Logger.mainLogger.error('Ticket verification failed:', errors); + return createApiError( + 'INVALID_TICKET_SIGNATURES', + 'Ticket verification failed', + errors + ); +} + +function validateTicketsArray(tickets: unknown): tickets is Ticket[] { + if (!Array.isArray(tickets)) { + Logger.mainLogger.error('Tickets data is not an array'); + return false; } + return true; +} - // Load config if allowedTicketSigners not provided - if (!allowedTicketSigners) { - const config = JSON.parse(readFileSync(configPath, 'utf8')); - allowedTicketSigners = config.allowedTicketSigners || {}; +function readAndValidateTickets(): Ticket[] { + const now = Date.now(); + + // Return cached tickets if they're still fresh + if (ticketCache && (now - ticketCache.lastRead) < CACHE_TTL) { + return ticketCache.tickets; } - const minSigRequired = 5; - const requiredSecurityLevel = DevSecurityLevel.HIGH; - const errors: VerificationError[] = []; + try { + // Read from disk + const jsonData = readFileSync(ticketFilePath, 'utf8'); + const tickets = JSON.parse(jsonData); - // Continue with signature verification - for (const ticket of tickets) { - const { data, sign, type } = ticket; - - const messageObj = { - data, - type - }; - - const verificationResult = verifyMultiSigs( - messageObj, - sign, - allowedTicketSigners, - minSigRequired, - requiredSecurityLevel - ); + if (!validateTicketsArray(tickets)) { + throw new Error('Invalid tickets format'); + } + // Verify tickets before caching + const verificationResult = verifyTickets(tickets, verificationConfig); if (!verificationResult.isValid) { - errors.push({ - type, - message: `Invalid signatures for ticket type ${type}. ` + - `Found ${verificationResult.validCount} valid signatures, ` + - `required ${minSigRequired} with security level HIGH`, - validSignatures: verificationResult.validCount - }); + throw new Error('Ticket verification failed'); } - } - - return { - isValid: errors.length === 0, - errors - }; -} -// Initialize Ajv with strict mode -const ajv = new Ajv({ - allErrors: true -}) + // Update cache + ticketCache = { + tickets, + lastRead: now + }; -// Add the schema -const validateTicketSchema = ajv.compile(ticketSchema) + return tickets; + } catch (err) { + if (err instanceof Error) { + throw err; // Re-throw to be handled by the route handlers + } + throw new Error('Unknown error reading tickets'); + } +} export const ticketsRouter: FastifyPluginCallback = function (fastify, opts, done) { // GET / - Get all tickets fastify.get('/', (_request, reply) => { try { - const filePath = join(__dirname, '..', '..', config.STATIC_FILES.TICKETS_JSON) - let jsonData: string - - try { - jsonData = readFileSync(filePath, 'utf8') - } catch (err) { - Logger.mainLogger.error('Failed to read tickets file:', err) - reply.code(500).send({ - error: 'Unable to access tickets configuration', - code: 'TICKETS_FILE_NOT_ACCESSIBLE' - }) - return - } - - try { - const tickets = JSON.parse(jsonData) - if (!Array.isArray(tickets)) { - Logger.mainLogger.error('Tickets data is not an array') - reply.code(500).send({ - error: 'Invalid tickets configuration format', - code: 'INVALID_TICKETS_FORMAT' - }) - return + const tickets = readAndValidateTickets(); + return reply.send(tickets); + } catch (err) { + if (err instanceof Error) { + if (err.message.includes('ENOENT')) { + const error = handleFileError(err); + return reply.code(error.statusCode).send(error.response); } - - // Verify tickets before returning - const verificationResult = verifyTickets(tickets, config.allowedTicketSigners || {}); - if (!verificationResult.isValid) { - Logger.mainLogger.error('Ticket verification failed:', verificationResult.errors) - reply.code(400).send({ - error: 'Ticket verification failed', - code: 'INVALID_TICKET_SIGNATURES', - details: verificationResult.errors - }) - return + if (err instanceof SyntaxError) { + const error = handleJsonParseError(err); + return reply.code(error.statusCode).send(error.response); + } + if (err.message === 'Invalid tickets format') { + const error = createApiError( + 'INVALID_TICKETS_FORMAT', + 'Invalid tickets configuration format' + ); + return reply.code(error.statusCode).send(error.response); + } + if (err.message === 'Ticket verification failed') { + const error = createApiError( + 'INVALID_TICKET_SIGNATURES', + 'Ticket verification failed' + ); + return reply.code(error.statusCode).send(error.response); } - - reply.send(tickets) - } catch (err) { - Logger.mainLogger.error('Failed to parse tickets JSON:', err) - reply.code(500).send({ - error: 'Invalid tickets configuration data', - code: 'INVALID_TICKETS_DATA' - }) } - } catch (err) { - Logger.mainLogger.error('Unexpected error in GET /tickets:', err) - reply.code(500).send({ - error: 'Internal server error', - code: 'INTERNAL_SERVER_ERROR' - }) + + const error = createApiError( + 'INTERNAL_SERVER_ERROR', + 'Internal server error' + ); + return reply.code(error.statusCode).send(error.response); } - }) + }); // GET /:type - Get tickets by type fastify.get('/:type', (request, reply) => { - try { - const { type } = request.params as { type: string } - - if (!type || typeof type !== 'string') { - reply.code(400).send({ - error: 'Invalid ticket type parameter', - code: 'INVALID_TICKET_TYPE' - }) - return - } + const { type } = request.params as { type: string }; + + if (!type || typeof type !== 'string') { + const error = createApiError( + 'INVALID_TICKET_TYPE', + 'Invalid ticket type parameter' + ); + return reply.code(error.statusCode).send(error.response); + } - const filePath = join(__dirname, '..', '..', config.STATIC_FILES.TICKETS_JSON) - let jsonData: string + try { + const tickets = readAndValidateTickets(); + const ticket = tickets.find((t) => t.type === type); - try { - jsonData = readFileSync(filePath, 'utf8') - } catch (err) { - Logger.mainLogger.error('Failed to read tickets file:', err) - reply.code(500).send({ - error: 'Unable to access tickets configuration', - code: 'TICKETS_FILE_NOT_ACCESSIBLE' - }) - return + if (!ticket) { + const error = createApiError( + 'TICKET_NOT_FOUND', + `No ticket found with type: ${type}` + ); + return reply.code(error.statusCode).send(error.response); } - try { - const tickets = JSON.parse(jsonData) - if (!Array.isArray(tickets)) { - Logger.mainLogger.error('Tickets data is not an array') - reply.code(500).send({ - error: 'Invalid tickets configuration format', - code: 'INVALID_TICKETS_FORMAT' - }) - return + return reply.send(ticket); + } catch (err) { + if (err instanceof Error) { + if (err.message.includes('ENOENT')) { + const error = handleFileError(err); + return reply.code(error.statusCode).send(error.response); } - - const ticket = tickets.find((t: { type: string }) => t.type === type) - - if (!ticket) { - reply.code(404).send({ - error: `No ticket found with type: ${type}`, - code: 'TICKET_NOT_FOUND' - }) - return + if (err instanceof SyntaxError) { + const error = handleJsonParseError(err); + return reply.code(error.statusCode).send(error.response); } - - // Verify single ticket before returning - const verificationResult = verifyTickets([ticket], config.allowedTicketSigners || {}); - if (!verificationResult.isValid) { - Logger.mainLogger.error('Ticket verification failed:', verificationResult.errors) - reply.code(400).send({ - error: 'Ticket verification failed', - code: 'INVALID_TICKET_SIGNATURES', - details: verificationResult.errors - }) - return + if (err.message === 'Invalid tickets format') { + const error = createApiError( + 'INVALID_TICKETS_FORMAT', + 'Invalid tickets configuration format' + ); + return reply.code(error.statusCode).send(error.response); + } + if (err.message === 'Ticket verification failed') { + const error = createApiError( + 'INVALID_TICKET_SIGNATURES', + 'Ticket verification failed' + ); + return reply.code(error.statusCode).send(error.response); } - - reply.send(ticket) - } catch (err) { - Logger.mainLogger.error('Failed to parse tickets JSON:', err) - reply.code(500).send({ - error: 'Invalid tickets configuration data', - code: 'INVALID_TICKETS_DATA' - }) } - } catch (err) { - Logger.mainLogger.error('Unexpected error in GET /tickets/:type:', err) - reply.code(500).send({ - error: 'Internal server error', - code: 'INTERNAL_SERVER_ERROR' - }) + + const error = createApiError( + 'INTERNAL_SERVER_ERROR', + 'Internal server error' + ); + return reply.code(error.statusCode).send(error.response); } - }) + }); - done() -} + done(); +}; export default ticketsRouter \ No newline at end of file diff --git a/src/schemas/ticketSchema.ts b/src/schemas/ticketSchema.ts index 49c5397..899bb70 100644 --- a/src/schemas/ticketSchema.ts +++ b/src/schemas/ticketSchema.ts @@ -1,6 +1,4 @@ -type TicketData = { - address: string; -} +import { TicketData } from '../types/tickets' type Sign = { owner: string; @@ -62,4 +60,4 @@ export const ticketSchema = { } } as const -export type { TicketData, Sign, Ticket } \ No newline at end of file +export type { Sign, Ticket } \ No newline at end of file diff --git a/src/services/ticketVerification.ts b/src/services/ticketVerification.ts new file mode 100644 index 0000000..7c572de --- /dev/null +++ b/src/services/ticketVerification.ts @@ -0,0 +1,121 @@ +import { ethers } from 'ethers' +import { Utils } from '@shardus/types' +import { Ticket, Sign } from '../schemas/ticketSchema' +import { DevSecurityLevel } from '../types/security' +import * as Ajv from 'ajv' +import { ticketSchema } from '../schemas/ticketSchema' + +export interface VerificationError { + type: string; + message: string; + validSignatures: number; +} + +export interface VerificationConfig { + allowedTicketSigners: { [pubkey: string]: DevSecurityLevel }; + minSigRequired: number; + requiredSecurityLevel: DevSecurityLevel; +} + +const ajv = new Ajv({ allErrors: true }) +const validateTicketSchema = ajv.compile(ticketSchema) + +function validateVerificationConfig(config: VerificationConfig): void { + if (!config.allowedTicketSigners || typeof config.allowedTicketSigners !== 'object') { + throw new Error('Invalid allowedTicketSigners configuration'); + } + if (typeof config.minSigRequired !== 'number' || config.minSigRequired < 1) { + throw new Error('minSigRequired must be a positive number'); + } + if (typeof config.requiredSecurityLevel !== 'number') { + throw new Error('Invalid requiredSecurityLevel'); + } +} + +export function verifyMultiSigs( + rawPayload: object, + sigs: Sign[], + config: VerificationConfig +): { isValid: boolean; validCount: number } { + validateVerificationConfig(config); + + if (sigs.length < config.minSigRequired) { + return { isValid: false, validCount: 0 }; + } + + if (sigs.length > Object.keys(config.allowedTicketSigners).length) { + return { isValid: false, validCount: 0 }; + } + + let validSigs = 0; + const message = Utils.safeStringify(rawPayload); + const hash = ethers.keccak256(ethers.toUtf8Bytes(message)); + const seen = new Set(); + + for (const sig of sigs) { + try { + const recoveredAddress = ethers.verifyMessage(hash, sig.sig); + + if ( + !seen.has(sig.owner) && + config.allowedTicketSigners[sig.owner] && + config.allowedTicketSigners[sig.owner] >= config.requiredSecurityLevel && + recoveredAddress.toLowerCase() === sig.owner.toLowerCase() + ) { + validSigs++; + seen.add(sig.owner); + } + } catch (error) { + continue; + } + } + + return { + isValid: validSigs >= config.minSigRequired, + validCount: validSigs + }; +} + +export function verifyTickets( + tickets: Ticket[], + config: VerificationConfig +): { isValid: boolean; errors: VerificationError[] } { + validateVerificationConfig(config); + + if (!validateTicketSchema(tickets)) { + return { + isValid: false, + errors: [{ + type: 'schema', + message: `Schema validation failed: ${ajv.errorsText(validateTicketSchema.errors)}`, + validSignatures: 0 + }] + }; + } + + const errors: VerificationError[] = []; + + for (const ticket of tickets) { + const { data, sign, type } = ticket; + const messageObj = { data, type }; + + const verificationResult = verifyMultiSigs( + messageObj, + sign, + config + ); + + if (!verificationResult.isValid) { + errors.push({ + type, + message: `Invalid signatures for ticket type ${type}. Found ${verificationResult.validCount} valid signatures, required ${config.minSigRequired} with security level ${DevSecurityLevel[config.requiredSecurityLevel]}`, + validSignatures: verificationResult.validCount + }); + } + } + + return { + isValid: errors.length === 0, + errors + }; +} \ No newline at end of file diff --git a/src/types/errors.ts b/src/types/errors.ts new file mode 100644 index 0000000..bc7d732 --- /dev/null +++ b/src/types/errors.ts @@ -0,0 +1,20 @@ +export interface ApiError { + statusCode: number; + response: { + error: string; + code: string; + details?: unknown; + }; +} + +export const ErrorCodes = { + TICKETS_FILE_NOT_ACCESSIBLE: 'TICKETS_FILE_NOT_ACCESSIBLE', + INVALID_TICKETS_FORMAT: 'INVALID_TICKETS_FORMAT', + INVALID_TICKETS_DATA: 'INVALID_TICKETS_DATA', + INVALID_TICKET_SIGNATURES: 'INVALID_TICKET_SIGNATURES', + TICKET_NOT_FOUND: 'TICKET_NOT_FOUND', + INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR', + INVALID_TICKET_TYPE: 'INVALID_TICKET_TYPE' +} as const; + +export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes]; \ No newline at end of file diff --git a/src/types/security.ts b/src/types/security.ts new file mode 100644 index 0000000..cf0bdf9 --- /dev/null +++ b/src/types/security.ts @@ -0,0 +1,6 @@ +export enum DevSecurityLevel { + NONE = 0, + LOW = 1, + MEDIUM = 2, + HIGH = 3 +} \ No newline at end of file diff --git a/src/types/tickets.ts b/src/types/tickets.ts new file mode 100644 index 0000000..727e94b --- /dev/null +++ b/src/types/tickets.ts @@ -0,0 +1,3 @@ +export interface TicketData { + address: string; +} \ No newline at end of file diff --git a/test/unit/src/routes/tickets.test.ts b/test/unit/src/routes/tickets.test.ts index 28f9116..e400a45 100644 --- a/test/unit/src/routes/tickets.test.ts +++ b/test/unit/src/routes/tickets.test.ts @@ -1,118 +1,203 @@ -import { FastifyInstance } from 'fastify' -import { readFileSync } from 'fs' -import { join } from 'path' -import { config } from '../../../../src/Config' -import { ticketsRouter } from '../../../../src/routes/tickets' +// Mock modules before importing routes +jest.mock('fs', () => ({ + readFileSync: jest.fn() +})); -// Mock Logger jest.mock('../../../../src/Logger', () => ({ - mainLogger: { - debug: jest.fn(), - error: jest.fn(), - info: jest.fn() - } -})) + mainLogger: { + debug: jest.fn(), + error: jest.fn(), + info: jest.fn() + } +})); + +jest.mock('../../../../src/Config', () => ({ + config: { + allowedTicketSigners: { + "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47": 3 // HIGH = 3 + } + } +})); + +// Import after mocks +import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify' +import { readFileSync } from 'fs' +import { ticketsRouter, ticketCache } from '../../../../src/routes/tickets' + +const mockValidTickets = [{ + data: [{ address: "0x37a9FCf5628B1C198A01C9eDaB0BF5C4d453E928" }], + sign: [{ + owner: "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47", + sig: "0x5f1aad2caa2cca1f725715ed050b1928527f0c4eb815fb282fad113ca866a63568d9c003b5310e16de67103521bf284fda10728b4fffc66055c55fde5934438d1b" + }], + type: "silver" +}]; + +function createMockReply() { + const mockSend = jest.fn(); + const mockCode = jest.fn().mockReturnThis(); + const reply = { + send: mockSend, + code: mockCode, + header: jest.fn().mockReturnThis(), + status: jest.fn().mockReturnThis(), + type: jest.fn().mockReturnThis(), + } as unknown as FastifyReply; + + return { reply, send: mockSend, code: mockCode }; +} describe('Ticket Routes', () => { - // Read the actual tickets file once at the start - const tickets = JSON.parse( - readFileSync(join(process.cwd(), config.STATIC_FILES.TICKETS_JSON), 'utf8') - ) - - describe('GET /', () => { - it('should return all tickets', async () => { - const mockReply = { - send: jest.fn(), - code: jest.fn().mockReturnThis() - } - - const mockFastify = { - get: jest.fn() - } as unknown as FastifyInstance - - // Call the plugin with the required arguments - ticketsRouter(mockFastify, {}, (err) => { - if (err) throw err - }) - - // Get the handler that was registered - const handler = mockFastify.get.mock.calls[0][1] - - // Call the handler directly - await handler({}, mockReply) - - expect(mockReply.send).toHaveBeenCalledWith(tickets) - expect(mockFastify.get).toHaveBeenCalledWith('/', expect.any(Function)) - }) - }) - - describe('GET /:type', () => { - it('should return silver ticket when requested', async () => { - const mockReply = { - send: jest.fn(), - code: jest.fn().mockReturnThis() - } - - const mockFastify = { - get: jest.fn() - } as unknown as FastifyInstance - - ticketsRouter(mockFastify, {}, (err) => { - if (err) throw err - }) - - const handler = mockFastify.get.mock.calls[1][1] - await handler({ params: { type: 'silver' } }, mockReply) - - expect(mockReply.send).toHaveBeenCalledWith(tickets[0]) - }) - - it('should return 404 for non-existent ticket type', async () => { - const mockReply = { - send: jest.fn(), - code: jest.fn().mockReturnThis() - } - - const mockFastify = { - get: jest.fn() - } as unknown as FastifyInstance - - ticketsRouter(mockFastify, {}, (err) => { - if (err) throw err - }) - - const handler = mockFastify.get.mock.calls[1][1] - await handler({ params: { type: 'gold' } }, mockReply) - - expect(mockReply.code).toHaveBeenCalledWith(404) - expect(mockReply.send).toHaveBeenCalledWith({ - error: 'No ticket found with type: gold', - code: 'TICKET_NOT_FOUND' - }) - }) - - it('should handle invalid type parameter', async () => { - const mockReply = { - send: jest.fn(), - code: jest.fn().mockReturnThis() - } - - const mockFastify = { - get: jest.fn() - } as unknown as FastifyInstance - - ticketsRouter(mockFastify, {}, (err) => { - if (err) throw err - }) - - const handler = mockFastify.get.mock.calls[1][1] - await handler({ params: { type: undefined } }, mockReply) - - expect(mockReply.code).toHaveBeenCalledWith(400) - expect(mockReply.send).toHaveBeenCalledWith({ - error: 'Invalid ticket type parameter', - code: 'INVALID_TICKET_TYPE' - }) - }) - }) -}) \ No newline at end of file + let mockFastify: FastifyInstance; + let routes: { [key: string]: Function } = {}; + + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + + // Reset the cache + (ticketCache as any) = null; + + // Create mock Fastify instance + mockFastify = { + get: jest.fn((path: string, handler: Function) => { + routes[path] = handler; + }) + } as unknown as FastifyInstance; + + // Initialize the router + ticketsRouter(mockFastify, {}, (err) => { + if (err) throw err; + }); + + // Default mock implementation + (readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockValidTickets)); + }); + + describe('GET /', () => { + it('should handle file not found error', async () => { + const { reply, send, code } = createMockReply(); + (readFileSync as jest.Mock).mockImplementationOnce(() => { + throw new Error('ENOENT: no such file'); + }); + + await routes['/']( + {} as FastifyRequest, + reply + ); + + expect(code).toHaveBeenCalledWith(500); + expect(send).toHaveBeenCalledWith(expect.objectContaining({ + code: 'TICKETS_FILE_NOT_ACCESSIBLE' + })); + }); + + it('should handle invalid JSON format', async () => { + const { reply, send, code } = createMockReply(); + (readFileSync as jest.Mock).mockReturnValueOnce('invalid json'); + + await routes['/']( + {} as FastifyRequest, + reply + ); + + expect(code).toHaveBeenCalledWith(400); + expect(send).toHaveBeenCalledWith(expect.objectContaining({ + code: 'INVALID_TICKETS_DATA' + })); + }); + + it('should handle non-array tickets data', async () => { + const { reply, send, code } = createMockReply(); + (readFileSync as jest.Mock).mockReturnValueOnce('{"not": "an array"}'); + + await routes['/']( + {} as FastifyRequest, + reply + ); + + expect(code).toHaveBeenCalledWith(400); + expect(send).toHaveBeenCalledWith(expect.objectContaining({ + code: 'INVALID_TICKETS_FORMAT' + })); + }); + }); + + describe('GET /:type', () => { + it('should use cached tickets for type lookup', async () => { + const { reply } = createMockReply(); + + // First call to populate cache + await routes['/:type']( + { params: { type: 'silver' } } as unknown as FastifyRequest, + reply + ); + expect(readFileSync).toHaveBeenCalledTimes(1); + + // Reset the mock call count + (readFileSync as jest.Mock).mockClear(); + + // Second call should use cache + await routes['/:type']( + { params: { type: 'silver' } } as unknown as FastifyRequest, + reply + ); + expect(readFileSync).toHaveBeenCalledTimes(0); + }); + }); + + describe('Cache invalidation', () => { + it('should reload tickets after TTL expires', async () => { + const { reply } = createMockReply(); + + // First call to populate cache + await routes['/']( + {} as FastifyRequest, + reply + ); + + // Verify first call + expect(readFileSync).toHaveBeenCalledTimes(1); + + // Reset mock + (readFileSync as jest.Mock).mockClear(); + + // Force cache invalidation by setting an old timestamp + (ticketCache as any) = { + tickets: mockValidTickets, + lastRead: Date.now() - (70 * 1000) // 70 seconds ago + }; + + // Second call should reload due to expired cache + await routes['/']( + {} as FastifyRequest, + reply + ); + + // Verify second call + expect(readFileSync).toHaveBeenCalledTimes(1); + }); + + it('should use cache within TTL', async () => { + const { reply } = createMockReply(); + + // First call to populate cache + await routes['/']( + {} as FastifyRequest, + reply + ); + + // Reset mock + (readFileSync as jest.Mock).mockClear(); + + // Second call within TTL should use cache + await routes['/']( + {} as FastifyRequest, + reply + ); + + // Should not read from file again + expect(readFileSync).not.toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/src/services/ticketVerification.test.ts b/test/unit/src/services/ticketVerification.test.ts new file mode 100644 index 0000000..9f18999 --- /dev/null +++ b/test/unit/src/services/ticketVerification.test.ts @@ -0,0 +1,203 @@ +import { verifyMultiSigs, verifyTickets, VerificationConfig } from '../../../../src/services/ticketVerification' +import { DevSecurityLevel } from '../../../../src/types/security' +import { Utils } from '@shardus/types' +import { ethers } from 'ethers' + +describe('Ticket Verification Service', () => { + // Create real wallets for testing with valid private keys + const signer1 = new ethers.Wallet('0x' + '1'.repeat(64)); + const signer2 = new ethers.Wallet('0x' + '2'.repeat(64)); + const signer3 = new ethers.Wallet('0x' + '3'.repeat(64)); + + const mockConfig: VerificationConfig = { + allowedTicketSigners: { + [signer1.address]: DevSecurityLevel.HIGH, + [signer2.address]: DevSecurityLevel.HIGH + }, + minSigRequired: 1, + requiredSecurityLevel: DevSecurityLevel.HIGH + }; + + const mockPayload = { + type: "silver", + data: [{ address: ethers.Wallet.createRandom().address }] + }; + + async function createSignature(wallet: ethers.Wallet, payload: object): Promise { + // Convert payload to string in a deterministic way and hash it + const message = Utils.safeStringify(payload); + const hash = ethers.keccak256(ethers.toUtf8Bytes(message)); + // Sign the hash directly + return wallet.signMessage(hash); + } + + let mockValidSigs: { owner: string; sig: string }[]; + + beforeEach(async () => { + // Create real signature for the payload + const sig = await createSignature(signer1, mockPayload); + mockValidSigs = [{ + owner: signer1.address, + sig + }]; + }); + + describe('verifyMultiSigs', () => { + it('should validate config parameters', () => { + const invalidConfigs = [ + { ...mockConfig, allowedTicketSigners: null }, + { ...mockConfig, minSigRequired: 0 }, + { ...mockConfig, minSigRequired: -1 }, + { ...mockConfig, requiredSecurityLevel: undefined } + ] as VerificationConfig[]; + + invalidConfigs.forEach(config => { + expect(() => verifyMultiSigs(mockPayload, mockValidSigs, config)) + .toThrow(); + }); + }); + + it('should return false if signatures count is less than required', async () => { + const config = { ...mockConfig, minSigRequired: 2 }; + const sig = await createSignature(signer1, mockPayload); + const sigs = [{ + owner: signer1.address, + sig + }]; + const result = verifyMultiSigs(mockPayload, sigs, config); + expect(result.isValid).toBe(false); + expect(result.validCount).toBe(0); + }); + + it('should return false if signatures count exceeds allowed signers', async () => { + const sig1 = await createSignature(signer1, mockPayload); + const sig2 = await createSignature(signer2, mockPayload); + const sig3 = await createSignature(signer3, mockPayload); + + const extraSigs = [ + { owner: signer1.address, sig: sig1 }, + { owner: signer2.address, sig: sig2 }, + { owner: signer3.address, sig: sig3 } + ]; + const result = verifyMultiSigs(mockPayload, extraSigs, mockConfig); + expect(result.isValid).toBe(false); + }); + + it('should verify signatures correctly', async () => { + const sig = await createSignature(signer1, mockPayload); + const sigs = [{ + owner: signer1.address, + sig + }]; + const result = verifyMultiSigs(mockPayload, sigs, mockConfig); + expect(result.isValid).toBe(true); + expect(result.validCount).toBe(1); + }); + + it('should handle duplicate signers', async () => { + const sig = await createSignature(signer1, mockPayload); + const duplicateSigs = [ + { owner: signer1.address, sig }, + { owner: signer1.address, sig } + ]; + const result = verifyMultiSigs(mockPayload, duplicateSigs, mockConfig); + expect(result.isValid).toBe(true); + expect(result.validCount).toBe(1); + }); + }); + + describe('verifyTickets', () => { + let mockValidTicket: any; + + beforeEach(async () => { + const ticketData = { + data: [{ address: ethers.Wallet.createRandom().address }], + type: "silver" + }; + const sig = await createSignature(signer1, ticketData); + mockValidTicket = { + ...ticketData, + sign: [{ + owner: signer1.address, + sig + }] + }; + }); + + it('should validate schema', () => { + const invalidTicket = { + data: [{ invalid: "field" }], + sign: mockValidSigs, + type: "silver" + }; + const result = verifyTickets([invalidTicket as any], mockConfig); + expect(result.isValid).toBe(false); + expect(result.errors[0].type).toBe('schema'); + }); + + it('should verify valid tickets', async () => { + const ticketData = { + data: [{ address: ethers.Wallet.createRandom().address }], + type: "silver" + }; + const sig = await createSignature(signer1, ticketData); + const validTicket = { + ...ticketData, + sign: [{ + owner: signer1.address, + sig + }] + }; + const result = verifyTickets([validTicket], mockConfig); + expect(result.isValid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it('should handle invalid signatures', async () => { + const invalidSig = await createSignature(signer3, mockPayload); + const invalidTicket = { + ...mockValidTicket, + sign: [{ + owner: signer3.address, + sig: invalidSig + }] + }; + const result = verifyTickets([invalidTicket], mockConfig); + expect(result.isValid).toBe(false); + expect(result.errors[0].type).toBe('silver'); + expect(result.errors[0].validSignatures).toBe(0); + }); + + it('should verify multiple tickets', async () => { + const ticketData = { + data: [{ address: ethers.Wallet.createRandom().address }], + type: "silver" + }; + const sig = await createSignature(signer1, ticketData); + const validTicket = { + ...ticketData, + sign: [{ + owner: signer1.address, + sig + }] + }; + const tickets = [validTicket, validTicket]; + const result = verifyTickets(tickets, mockConfig); + expect(result.isValid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it('should handle mixed valid and invalid tickets', async () => { + const invalidTicket = { + ...mockValidTicket, + type: "gold" + }; + + const tickets = [mockValidTicket, invalidTicket]; + const result = verifyTickets(tickets, mockConfig); + expect(result.isValid).toBe(false); + expect(result.errors).toHaveLength(1); + expect(result.errors[0].type).toBe('schema'); + }); + }); +}); \ No newline at end of file From bb1ab2334d72948ca1f5d739073e4788f6d2a5e2 Mon Sep 17 00:00:00 2001 From: Zachary Belford Date: Fri, 22 Nov 2024 16:21:32 -0800 Subject: [PATCH 06/12] fix: yet some more cleanup --- archiver-config.json | 14 ++-- src/Config.ts | 47 +++-------- src/routes/tickets.ts | 68 ++++++++-------- src/server.ts | 9 +++ src/services/ticketVerification.ts | 78 ++++++++++--------- src/types/tickets.ts | 2 +- test/unit/src/routes/tickets.test.ts | 13 +++- .../src/services/ticketVerification.test.ts | 23 ++---- 8 files changed, 122 insertions(+), 132 deletions(-) diff --git a/archiver-config.json b/archiver-config.json index c97dce3..85044d1 100644 --- a/archiver-config.json +++ b/archiver-config.json @@ -56,11 +56,13 @@ "publicKey": "aec5d2b663869d9c22ba99d8de76f3bff0f54fa5e39d2899ec1f3f4543422ec7" } ], - "ARCHIVER_MODE": "debug", + "ARCHIVER_MODE": "release", "DevPublicKey": "", - "allowedTicketSigners": { - "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47": 3 - }, - "minSigRequired": 5, - "requiredSecurityLevel": 3 + "tickets": { + "allowedTicketSigners": { + "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47": 3 + }, + "minSigRequired": 1, + "requiredSecurityLevel": 3 + } } \ No newline at end of file diff --git a/src/Config.ts b/src/Config.ts index 8f10e9e..9d60d88 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -102,8 +102,12 @@ export interface Config { disableOffloadReceipt: boolean // To disable offloading of receipts globally disableOffloadReceiptForGlobalModification: boolean // To disable offloading of receipts for global modifications receipts restoreNGTsFromSnapshot: boolean // To restore NGTs from snapshot - allowedTicketSigners: { - [pubkey: string]: number + tickets: { + allowedTicketSigners: { + [pubkey: string]: number + } + minSigRequired: number + requiredSecurityLevel: number } } @@ -134,7 +138,7 @@ let config: Config = { save: true, interval: 1, }, - ARCHIVER_MODE: 'debug', // 'debug'/'release' + ARCHIVER_MODE: 'release', // 'debug'/'release' DevPublicKey: '', dataLogWrite: true, dataLogWriter: { @@ -198,39 +202,10 @@ let config: Config = { disableOffloadReceipt: false, disableOffloadReceiptForGlobalModification: true, restoreNGTsFromSnapshot: false, - allowedTicketSigners: { // copied from shardeum config for now... - /* prettier-ignore */ '0x002D3a2BfE09E3E29b6d38d58CaaD16EEe4C9BC5': 5, - /* prettier-ignore */ '0x0a0844DA5e01E391d12999ca859Da8a897D5979A': 5, - /* prettier-ignore */ '0x390878B18DeBe2A9f0d5c0252a109c84243D3beb': 5, - /* prettier-ignore */ '0x32B6f2C027D4c9D99Ca07d047D17987390a5EB39': 5, - /* prettier-ignore */ '0x80aF8E195B56aCC3b4ec8e2C99EC38957258635a': 5, - /* prettier-ignore */ '0x7Efbb31431ac7C405E8eEba99531fF1254fCA3B6': 5, - /* prettier-ignore */ '0xCc74bf387F6C102b5a7F828796C57A6D2D19Cb00': 5, - /* prettier-ignore */ '0x4ed5C053BF2dA5F694b322EA93dce949F3276B85': 5, - /* prettier-ignore */ '0xd31aBC7497aD8bC9fe8555C9eDe45DFd7FB3Bf6F': 5, - /* prettier-ignore */ '0xe7e4cc292b424C6D50d16F1Bb5BAB2032c486980': 5, - /* prettier-ignore */ '0xD815DA50966c19261B34Ffa3bE50A30A67D97456': 5, - /* prettier-ignore */ '0xE856B2365641eba73Bc430AAC1E8F930dA513D9D': 5, - /* prettier-ignore */ '0x8282F755e784414697421D4b59232E5d194e2262': 5, - /* prettier-ignore */ '0x353Ad64Df4fAe5EffF717A1c41BE6dEBee543129': 5, - /* prettier-ignore */ '0x9Ce1C3c114538c625aA2488b97fEb3723fdBB07B': 5, - /* prettier-ignore */ '0x6A83e4e4eB0A2c8f562db6BB64b02a9A6237B314': 5, - /* prettier-ignore */ '0x92E375E0c76CaE76D9DfBab17EE7B3B4EE407715': 5, - /* prettier-ignore */ '0xBD79B430CA932e2D89bb77ACaE7367a07471c2eA': 5, - /* prettier-ignore */ '0xEbe173a837Bc30BFEF6E13C9988a4771a4D83275': 5, - /* prettier-ignore */ '0xfF2b584A947182c55BBc039BEAB78BC201D3AdDe': 5, - /* prettier-ignore */ '0xCeA068d8DCB4B4020D30a9950C00cF8408611F67': 5, - /* prettier-ignore */ '0x52F8d3DaA7b5FF25ca2bF7417E059aFe0bD5fB0E': 5, - /* prettier-ignore */ '0x0341996A92193d8B7d80C4774fA2eff889e4b427': 5, - /* prettier-ignore */ '0xF82BDA6Ef512e4219C6DCEea896E50e8180a5bff': 5, - /* prettier-ignore */ '0xA04A1B214a2537139fE59488820D4dA06516933f': 5, - /* prettier-ignore */ '0x550817e7B91244BBeFE2AD621ccD555A16B00405': 5, - /* prettier-ignore */ '0x84C55a4bFfff1ADadb9C46e2B60979F519dAf874': 5, - /* prettier-ignore */ '0x4563303BCE96D3f8d9C7fB94b36dfFC9d831871d': 5, - /* prettier-ignore */ '0xdA058F9c7Ce86C1D21DD5DBDeBad5ab5c785520a': 5, - /* prettier-ignore */ '0x0950C3Ecc7d1c4dd093C9652F335F9391d83Ee99': 5, - /* prettier-ignore */ '0x05b67C84bf88d93E795039d5bBA9CeC9Dcd93087': 5, - /* prettier-ignore */ '0x891DF765C855E9848A18Ed18984B9f57cb3a4d47': 5, // zb added this one for testing + tickets: { + allowedTicketSigners: {}, + minSigRequired: 5, + requiredSecurityLevel: 3 } } // Override default config params from config file, env vars, and cli args diff --git a/src/routes/tickets.ts b/src/routes/tickets.ts index e7dd6c1..5e9840a 100644 --- a/src/routes/tickets.ts +++ b/src/routes/tickets.ts @@ -3,14 +3,11 @@ import { readFileSync } from 'fs' import { join } from 'path' import { config } from '../Config' import * as Logger from '../Logger' -import * as Ajv from 'ajv' -import { ticketSchema, type Ticket } from '../schemas/ticketSchema' +import { type Ticket } from '../schemas/ticketSchema' import { verifyTickets, VerificationConfig, VerificationError } from '../services/ticketVerification' import { ApiError, ErrorCodes } from '../types/errors' import { DevSecurityLevel } from '../types/security' -const ajv = new Ajv({ allErrors: true }) -const validateTicketSchema = ajv.compile(ticketSchema) const ticketFilePath = join(__dirname, '..', '..', 'static', 'tickets.json') // Export for testing @@ -24,9 +21,9 @@ interface TicketCache { } const verificationConfig: VerificationConfig = { - allowedTicketSigners: config.allowedTicketSigners, - minSigRequired: process.env.NODE_ENV === 'production' ? 5 : 1, - requiredSecurityLevel: DevSecurityLevel.HIGH + allowedTicketSigners: config.tickets.allowedTicketSigners, + minSigRequired: config.tickets.minSigRequired, + requiredSecurityLevel: config.tickets.requiredSecurityLevel as DevSecurityLevel }; function createApiError(code: keyof typeof ErrorCodes, message: string, details?: unknown): ApiError { @@ -84,43 +81,52 @@ function validateTicketsArray(tickets: unknown): tickets is Ticket[] { function readAndValidateTickets(): Ticket[] { const now = Date.now(); - - // Return cached tickets if they're still fresh + + // Check if we have valid cached tickets if (ticketCache && (now - ticketCache.lastRead) < CACHE_TTL) { return ticketCache.tickets; } - try { - // Read from disk - const jsonData = readFileSync(ticketFilePath, 'utf8'); - const tickets = JSON.parse(jsonData); + const jsonData = readFileSync(ticketFilePath, 'utf8'); + const tickets = JSON.parse(jsonData); - if (!validateTicketsArray(tickets)) { - throw new Error('Invalid tickets format'); - } + if (!validateTicketsArray(tickets)) { + throw new Error('Invalid tickets format'); + } - // Verify tickets before caching - const verificationResult = verifyTickets(tickets, verificationConfig); - if (!verificationResult.isValid) { - throw new Error('Ticket verification failed'); - } + + const verificationResult = verifyTickets(tickets, verificationConfig); + if (!verificationResult.isValid) { + throw new Error('Ticket verification failed'); + } + // Update cache + ticketCache = { + tickets, + lastRead: now + }; - // Update cache - ticketCache = { - tickets, - lastRead: now - }; + return tickets; +} - return tickets; +export function initializeTickets(): void { + try { + readAndValidateTickets(); + Logger.mainLogger.info('Initial ticket verification successful'); } catch (err) { - if (err instanceof Error) { - throw err; // Re-throw to be handled by the route handlers - } - throw new Error('Unknown error reading tickets'); + Logger.mainLogger.error('Failed to verify tickets during initialization:', err); + throw err; // This will prevent server from starting if tickets are invalid } } export const ticketsRouter: FastifyPluginCallback = function (fastify, opts, done) { + // Add initialization before route handlers + try { + initializeTickets(); + } catch (err) { + done(err as Error); + return; + } + // GET / - Get all tickets fastify.get('/', (_request, reply) => { try { diff --git a/src/server.ts b/src/server.ts index 4e678ba..fe360df 100644 --- a/src/server.ts +++ b/src/server.ts @@ -44,6 +44,7 @@ import { Utils as StringUtils } from '@shardus/types' import { healthCheckRouter } from './routes/healthCheck' import { setupWorkerProcesses } from './primary-process' import { initWorkerProcess } from './worker-process' +import { initializeTickets } from './routes/tickets'; const configFile = join(process.cwd(), 'archiver-config.json') let logDir: string @@ -488,4 +489,12 @@ async function startServer(): Promise { ) } +// Add this before starting the server +try { + initializeTickets(); +} catch (err) { + console.error('Failed to initialize tickets. Server startup aborted:', err); + process.exit(1); +} + start() diff --git a/src/services/ticketVerification.ts b/src/services/ticketVerification.ts index 7c572de..da698c1 100644 --- a/src/services/ticketVerification.ts +++ b/src/services/ticketVerification.ts @@ -31,51 +31,53 @@ function validateVerificationConfig(config: VerificationConfig): void { throw new Error('Invalid requiredSecurityLevel'); } } - export function verifyMultiSigs( rawPayload: object, sigs: Sign[], - config: VerificationConfig + allowedPubkeys: { [pubkey: string]: DevSecurityLevel }, + minSigRequired: number, + requiredSecurityLevel: DevSecurityLevel ): { isValid: boolean; validCount: number } { - validateVerificationConfig(config); - - if (sigs.length < config.minSigRequired) { - return { isValid: false, validCount: 0 }; + if (!rawPayload || !sigs || !allowedPubkeys || !Array.isArray(sigs)) { + return { isValid: false, validCount: 0 } } - - if (sigs.length > Object.keys(config.allowedTicketSigners).length) { - return { isValid: false, validCount: 0 }; + if (sigs.length < minSigRequired) return { isValid: false, validCount: 0 } + + // no reason to allow more signatures than allowedPubkeys exist + // this also prevent loop exhaustion + if (sigs.length > Object.keys(allowedPubkeys).length) return { isValid: false, validCount: 0 } + + let validSigs = 0 + const payload_hash = ethers.keccak256(ethers.toUtf8Bytes(Utils.safeStringify(rawPayload))) + const seen = new Set() + + for (let i = 0; i < sigs.length; i++) { + /* eslint-disable security/detect-object-injection */ + // The sig owner has not been seen before + // The sig owner is listed on the server + // The sig owner has enough security clearance + // The signature is valid + if ( + !seen.has(sigs[i].owner) && + allowedPubkeys[sigs[i].owner] && + allowedPubkeys[sigs[i].owner] >= requiredSecurityLevel && + ethers.verifyMessage(payload_hash, sigs[i].sig).toLowerCase() === sigs[i].owner.toLowerCase() + ) { + validSigs++ + seen.add(sigs[i].owner) + } + /* eslint-enable security/detect-object-injection */ + + if (validSigs >= minSigRequired) break } - - let validSigs = 0; - const message = Utils.safeStringify(rawPayload); - const hash = ethers.keccak256(ethers.toUtf8Bytes(message)); - const seen = new Set(); - - for (const sig of sigs) { - try { - const recoveredAddress = ethers.verifyMessage(hash, sig.sig); - - if ( - !seen.has(sig.owner) && - config.allowedTicketSigners[sig.owner] && - config.allowedTicketSigners[sig.owner] >= config.requiredSecurityLevel && - recoveredAddress.toLowerCase() === sig.owner.toLowerCase() - ) { - validSigs++; - seen.add(sig.owner); - } - } catch (error) { - continue; - } + + return { + isValid: validSigs >= minSigRequired, + validCount: validSigs } - - return { - isValid: validSigs >= config.minSigRequired, - validCount: validSigs - }; } + export function verifyTickets( tickets: Ticket[], config: VerificationConfig @@ -102,7 +104,9 @@ export function verifyTickets( const verificationResult = verifyMultiSigs( messageObj, sign, - config + config.allowedTicketSigners, + config.minSigRequired, + config.requiredSecurityLevel ); if (!verificationResult.isValid) { diff --git a/src/types/tickets.ts b/src/types/tickets.ts index 727e94b..2acf6b5 100644 --- a/src/types/tickets.ts +++ b/src/types/tickets.ts @@ -1,3 +1,3 @@ -export interface TicketData { +export type TicketData = { address: string; } \ No newline at end of file diff --git a/test/unit/src/routes/tickets.test.ts b/test/unit/src/routes/tickets.test.ts index e400a45..e7b66fb 100644 --- a/test/unit/src/routes/tickets.test.ts +++ b/test/unit/src/routes/tickets.test.ts @@ -13,8 +13,12 @@ jest.mock('../../../../src/Logger', () => ({ jest.mock('../../../../src/Config', () => ({ config: { - allowedTicketSigners: { - "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47": 3 // HIGH = 3 + tickets: { + allowedTicketSigners: { + "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47": 3 // HIGH = 3 + }, + minSigRequired: 1, + requiredSecurityLevel: 3 } } })); @@ -126,6 +130,9 @@ describe('Ticket Routes', () => { describe('GET /:type', () => { it('should use cached tickets for type lookup', async () => { const { reply } = createMockReply(); + + // Set up mock to return valid tickets + (readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockValidTickets)); // First call to populate cache await routes['/:type']( @@ -134,7 +141,7 @@ describe('Ticket Routes', () => { ); expect(readFileSync).toHaveBeenCalledTimes(1); - // Reset the mock call count + // Reset the mock call count but keep the same implementation (readFileSync as jest.Mock).mockClear(); // Second call should use cache diff --git a/test/unit/src/services/ticketVerification.test.ts b/test/unit/src/services/ticketVerification.test.ts index 9f18999..987206b 100644 --- a/test/unit/src/services/ticketVerification.test.ts +++ b/test/unit/src/services/ticketVerification.test.ts @@ -43,19 +43,6 @@ describe('Ticket Verification Service', () => { }); describe('verifyMultiSigs', () => { - it('should validate config parameters', () => { - const invalidConfigs = [ - { ...mockConfig, allowedTicketSigners: null }, - { ...mockConfig, minSigRequired: 0 }, - { ...mockConfig, minSigRequired: -1 }, - { ...mockConfig, requiredSecurityLevel: undefined } - ] as VerificationConfig[]; - - invalidConfigs.forEach(config => { - expect(() => verifyMultiSigs(mockPayload, mockValidSigs, config)) - .toThrow(); - }); - }); it('should return false if signatures count is less than required', async () => { const config = { ...mockConfig, minSigRequired: 2 }; @@ -64,9 +51,9 @@ describe('Ticket Verification Service', () => { owner: signer1.address, sig }]; - const result = verifyMultiSigs(mockPayload, sigs, config); + const result = verifyMultiSigs(mockPayload, sigs, config.allowedTicketSigners, config.minSigRequired, config.requiredSecurityLevel); expect(result.isValid).toBe(false); - expect(result.validCount).toBe(0); + expect(result.validCount).toBe(0); // we early return if signatures count is less than required }); it('should return false if signatures count exceeds allowed signers', async () => { @@ -79,7 +66,7 @@ describe('Ticket Verification Service', () => { { owner: signer2.address, sig: sig2 }, { owner: signer3.address, sig: sig3 } ]; - const result = verifyMultiSigs(mockPayload, extraSigs, mockConfig); + const result = verifyMultiSigs(mockPayload, extraSigs, mockConfig.allowedTicketSigners, mockConfig.minSigRequired, mockConfig.requiredSecurityLevel); expect(result.isValid).toBe(false); }); @@ -89,7 +76,7 @@ describe('Ticket Verification Service', () => { owner: signer1.address, sig }]; - const result = verifyMultiSigs(mockPayload, sigs, mockConfig); + const result = verifyMultiSigs(mockPayload, sigs, mockConfig.allowedTicketSigners, mockConfig.minSigRequired, mockConfig.requiredSecurityLevel); expect(result.isValid).toBe(true); expect(result.validCount).toBe(1); }); @@ -100,7 +87,7 @@ describe('Ticket Verification Service', () => { { owner: signer1.address, sig }, { owner: signer1.address, sig } ]; - const result = verifyMultiSigs(mockPayload, duplicateSigs, mockConfig); + const result = verifyMultiSigs(mockPayload, duplicateSigs, mockConfig.allowedTicketSigners, mockConfig.minSigRequired, mockConfig.requiredSecurityLevel); expect(result.isValid).toBe(true); expect(result.validCount).toBe(1); }); From f60e2576a553a03b503b4f3427982b7174837ed5 Mon Sep 17 00:00:00 2001 From: Zachary Belford Date: Fri, 22 Nov 2024 16:40:17 -0800 Subject: [PATCH 07/12] fix: errors during initialization checks --- src/Config.ts | 6 ++++-- src/routes/tickets.ts | 5 +---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Config.ts b/src/Config.ts index 9d60d88..d5647df 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -203,8 +203,10 @@ let config: Config = { disableOffloadReceiptForGlobalModification: true, restoreNGTsFromSnapshot: false, tickets: { - allowedTicketSigners: {}, - minSigRequired: 5, + allowedTicketSigners: { + "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47": 3 + }, + minSigRequired: 1, requiredSecurityLevel: 3 } } diff --git a/src/routes/tickets.ts b/src/routes/tickets.ts index 5e9840a..a6f99f4 100644 --- a/src/routes/tickets.ts +++ b/src/routes/tickets.ts @@ -94,10 +94,9 @@ function readAndValidateTickets(): Ticket[] { throw new Error('Invalid tickets format'); } - const verificationResult = verifyTickets(tickets, verificationConfig); if (!verificationResult.isValid) { - throw new Error('Ticket verification failed'); + throw new Error(`Ticket verification failed: ${verificationResult.errors.map(e => e.message).join(', ')}`); } // Update cache ticketCache = { @@ -111,9 +110,7 @@ function readAndValidateTickets(): Ticket[] { export function initializeTickets(): void { try { readAndValidateTickets(); - Logger.mainLogger.info('Initial ticket verification successful'); } catch (err) { - Logger.mainLogger.error('Failed to verify tickets during initialization:', err); throw err; // This will prevent server from starting if tickets are invalid } } From a99115d4b5216ba21e44fa340a9949990bd026eb Mon Sep 17 00:00:00 2001 From: Zachary Belford Date: Tue, 26 Nov 2024 11:55:14 -0800 Subject: [PATCH 08/12] fix: add ticket list using multisig --- src/Config.ts | 35 ++++++++++++- static/tickets.json | 124 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 144 insertions(+), 15 deletions(-) diff --git a/src/Config.ts b/src/Config.ts index d5647df..61311c5 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -4,6 +4,7 @@ import * as merge from 'deepmerge' import * as minimist from 'minimist' import { join } from 'path' import { Utils as StringUtils } from '@shardus/types' +import { DevSecurityLevel } from './types/security' export interface Config { [index: string]: object | string | number | boolean @@ -204,7 +205,39 @@ let config: Config = { restoreNGTsFromSnapshot: false, tickets: { allowedTicketSigners: { - "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47": 3 + '0x002D3a2BfE09E3E29b6d38d58CaaD16EEe4C9BC5': 5, + '0x0a0844DA5e01E391d12999ca859Da8a897D5979A': 5, + '0x390878B18DeBe2A9f0d5c0252a109c84243D3beb': 5, + '0x32B6f2C027D4c9D99Ca07d047D17987390a5EB39': 5, + '0x80aF8E195B56aCC3b4ec8e2C99EC38957258635a': 5, + '0x7Efbb31431ac7C405E8eEba99531fF1254fCA3B6': 5, + '0xCc74bf387F6C102b5a7F828796C57A6D2D19Cb00': 5, + '0x4ed5C053BF2dA5F694b322EA93dce949F3276B85': 5, + '0xd31aBC7497aD8bC9fe8555C9eDe45DFd7FB3Bf6F': 5, + '0xe7e4cc292b424C6D50d16F1Bb5BAB2032c486980': 5, + '0xD815DA50966c19261B34Ffa3bE50A30A67D97456': 5, + '0xE856B2365641eba73Bc430AAC1E8F930dA513D9D': 5, + '0x8282F755e784414697421D4b59232E5d194e2262': 5, + '0x353Ad64Df4fAe5EffF717A1c41BE6dEBee543129': 5, + '0x9Ce1C3c114538c625aA2488b97fEb3723fdBB07B': 5, + '0x6A83e4e4eB0A2c8f562db6BB64b02a9A6237B314': 5, + '0x92E375E0c76CaE76D9DfBab17EE7B3B4EE407715': 5, + '0xBD79B430CA932e2D89bb77ACaE7367a07471c2eA': 5, + '0xEbe173a837Bc30BFEF6E13C9988a4771a4D83275': 5, + '0xfF2b584A947182c55BBc039BEAB78BC201D3AdDe': 5, + '0xCeA068d8DCB4B4020D30a9950C00cF8408611F67': 5, + '0x52F8d3DaA7b5FF25ca2bF7417E059aFe0bD5fB0E': 5, + '0x0341996A92193d8B7d80C4774fA2eff889e4b427': 5, + '0xF82BDA6Ef512e4219C6DCEea896E50e8180a5bff': 5, + '0xA04A1B214a2537139fE59488820D4dA06516933f': 5, + '0x550817e7B91244BBeFE2AD621ccD555A16B00405': 5, + '0x84C55a4bFfff1ADadb9C46e2B60979F519dAf874': 5, + '0x4563303BCE96D3f8d9C7fB94b36dfFC9d831871d': 5, + '0xdA058F9c7Ce86C1D21DD5DBDeBad5ab5c785520a': 5, + '0x891DF765C855E9848A18Ed18984B9f57cb3a4d47': 5, + '0x7Fb9b1C5E20bd250870F87659E46bED410221f17': 5, + '0x1e5e12568b7103E8B22cd680A6fa6256DD66ED76': 5, + '0xa58169308e7153B5Ce4ca5cA515cC4d0cBE7770B': 5, }, minSigRequired: 1, requiredSecurityLevel: 3 diff --git a/static/tickets.json b/static/tickets.json index 0424c79..8218a3c 100644 --- a/static/tickets.json +++ b/static/tickets.json @@ -1,16 +1,112 @@ [ - { - "data": [ - { - "address": "0x37a9FCf5628B1C198A01C9eDaB0BF5C4d453E928" - } - ], - "sign": [ - { - "owner": "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47", - "sig": "0x5f1aad2caa2cca1f725715ed050b1928527f0c4eb815fb282fad113ca866a63568d9c003b5310e16de67103521bf284fda10728b4fffc66055c55fde5934438d1b" - } - ], - "type": "silver" - } + { + "type": "silver", + "data": [ + { + "address": "0x002D3a2BfE09E3E29b6d38d58CaaD16EEe4C9BC5" + }, + { + "address": "0x0a0844DA5e01E391d12999ca859Da8a897D5979A" + }, + { + "address": "0x390878B18DeBe2A9f0d5c0252a109c84243D3beb" + }, + { + "address": "0x32B6f2C027D4c9D99Ca07d047D17987390a5EB39" + }, + { + "address": "0x80aF8E195B56aCC3b4ec8e2C99EC38957258635a" + }, + { + "address": "0x7Efbb31431ac7C405E8eEba99531fF1254fCA3B6" + }, + { + "address": "0xCc74bf387F6C102b5a7F828796C57A6D2D19Cb00" + }, + { + "address": "0x4ed5C053BF2dA5F694b322EA93dce949F3276B85" + }, + { + "address": "0xd31aBC7497aD8bC9fe8555C9eDe45DFd7FB3Bf6F" + }, + { + "address": "0xe7e4cc292b424C6D50d16F1Bb5BAB2032c486980" + }, + { + "address": "0xD815DA50966c19261B34Ffa3bE50A30A67D97456" + }, + { + "address": "0xE856B2365641eba73Bc430AAC1E8F930dA513D9D" + }, + { + "address": "0x8282F755e784414697421D4b59232E5d194e2262" + }, + { + "address": "0x353Ad64Df4fAe5EffF717A1c41BE6dEBee543129" + }, + { + "address": "0x9Ce1C3c114538c625aA2488b97fEb3723fdBB07B" + }, + { + "address": "0x6A83e4e4eB0A2c8f562db6BB64b02a9A6237B314" + }, + { + "address": "0x92E375E0c76CaE76D9DfBab17EE7B3B4EE407715" + }, + { + "address": "0xBD79B430CA932e2D89bb77ACaE7367a07471c2eA" + }, + { + "address": "0xEbe173a837Bc30BFEF6E13C9988a4771a4D83275" + }, + { + "address": "0xfF2b584A947182c55BBc039BEAB78BC201D3AdDe" + }, + { + "address": "0xCeA068d8DCB4B4020D30a9950C00cF8408611F67" + }, + { + "address": "0x52F8d3DaA7b5FF25ca2bF7417E059aFe0bD5fB0E" + }, + { + "address": "0x0341996A92193d8B7d80C4774fA2eff889e4b427" + }, + { + "address": "0xF82BDA6Ef512e4219C6DCEea896E50e8180a5bff" + }, + { + "address": "0xA04A1B214a2537139fE59488820D4dA06516933f" + }, + { + "address": "0x550817e7B91244BBeFE2AD621ccD555A16B00405" + }, + { + "address": "0x84C55a4bFfff1ADadb9C46e2B60979F519dAf874" + }, + { + "address": "0x4563303BCE96D3f8d9C7fB94b36dfFC9d831871d" + }, + { + "address": "0xdA058F9c7Ce86C1D21DD5DBDeBad5ab5c785520a" + }, + { + "address": "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47" + }, + { + "address": "0x7Fb9b1C5E20bd250870F87659E46bED410221f17" + }, + { + "address": "0x1e5e12568b7103E8B22cd680A6fa6256DD66ED76" + }, + { + "address": "0xa58169308e7153B5Ce4ca5cA515cC4d0cBE7770B" + } + ], + "sign": [ + { + "owner": "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47", + "sig": "0x9701d7ec08583a80981c6e4e2aee0bef44236bb08f35d42511561ee992ed7bae0c049fa781aa49e089da5ec4a1c61d5fc8393a5b39a34e35ac9ec47642e5de191c" + } + ] + } ] \ No newline at end of file From 0afed26da65c27784ec980cef6d32851c06919d1 Mon Sep 17 00:00:00 2001 From: Zachary Belford Date: Tue, 26 Nov 2024 12:21:07 -0800 Subject: [PATCH 09/12] fix: copy into default config --- archiver-config.json | 36 ++++++++++++++++++++++++++++++++++-- src/Config.ts | 2 +- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/archiver-config.json b/archiver-config.json index 85044d1..42a67ac 100644 --- a/archiver-config.json +++ b/archiver-config.json @@ -60,9 +60,41 @@ "DevPublicKey": "", "tickets": { "allowedTicketSigners": { - "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47": 3 + "0x002D3a2BfE09E3E29b6d38d58CaaD16EEe4C9BC5": 5, + "0x0a0844DA5e01E391d12999ca859Da8a897D5979A": 5, + "0x390878B18DeBe2A9f0d5c0252a109c84243D3beb": 5, + "0x32B6f2C027D4c9D99Ca07d047D17987390a5EB39": 5, + "0x80aF8E195B56aCC3b4ec8e2C99EC38957258635a": 5, + "0x7Efbb31431ac7C405E8eEba99531fF1254fCA3B6": 5, + "0xCc74bf387F6C102b5a7F828796C57A6D2D19Cb00": 5, + "0x4ed5C053BF2dA5F694b322EA93dce949F3276B85": 5, + "0xd31aBC7497aD8bC9fe8555C9eDe45DFd7FB3Bf6F": 5, + "0xe7e4cc292b424C6D50d16F1Bb5BAB2032c486980": 5, + "0xD815DA50966c19261B34Ffa3bE50A30A67D97456": 5, + "0xE856B2365641eba73Bc430AAC1E8F930dA513D9D": 5, + "0x8282F755e784414697421D4b59232E5d194e2262": 5, + "0x353Ad64Df4fAe5EffF717A1c41BE6dEBee543129": 5, + "0x9Ce1C3c114538c625aA2488b97fEb3723fdBB07B": 5, + "0x6A83e4e4eB0A2c8f562db6BB64b02a9A6237B314": 5, + "0x92E375E0c76CaE76D9DfBab17EE7B3B4EE407715": 5, + "0xBD79B430CA932e2D89bb77ACaE7367a07471c2eA": 5, + "0xEbe173a837Bc30BFEF6E13C9988a4771a4D83275": 5, + "0xfF2b584A947182c55BBc039BEAB78BC201D3AdDe": 5, + "0xCeA068d8DCB4B4020D30a9950C00cF8408611F67": 5, + "0x52F8d3DaA7b5FF25ca2bF7417E059aFe0bD5fB0E": 5, + "0x0341996A92193d8B7d80C4774fA2eff889e4b427": 5, + "0xF82BDA6Ef512e4219C6DCEea896E50e8180a5bff": 5, + "0xA04A1B214a2537139fE59488820D4dA06516933f": 5, + "0x550817e7B91244BBeFE2AD621ccD555A16B00405": 5, + "0x84C55a4bFfff1ADadb9C46e2B60979F519dAf874": 5, + "0x4563303BCE96D3f8d9C7fB94b36dfFC9d831871d": 5, + "0xdA058F9c7Ce86C1D21DD5DBDeBad5ab5c785520a": 5, + "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47": 5, + "0x7Fb9b1C5E20bd250870F87659E46bED410221f17": 5, + "0x1e5e12568b7103E8B22cd680A6fa6256DD66ED76": 5, + "0xa58169308e7153B5Ce4ca5cA515cC4d0cBE7770B": 5 }, "minSigRequired": 1, - "requiredSecurityLevel": 3 + "requiredSecurityLevel": 5 } } \ No newline at end of file diff --git a/src/Config.ts b/src/Config.ts index 61311c5..b6ba858 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -240,7 +240,7 @@ let config: Config = { '0xa58169308e7153B5Ce4ca5cA515cC4d0cBE7770B': 5, }, minSigRequired: 1, - requiredSecurityLevel: 3 + requiredSecurityLevel: 5 } } // Override default config params from config file, env vars, and cli args From 6c9ca55b43b486bc01adf5975bec46459c1cbd80 Mon Sep 17 00:00:00 2001 From: Shawn Ifill Date: Wed, 27 Nov 2024 11:49:18 -0500 Subject: [PATCH 10/12] SHARD-1097: update tickets.json with additional signature --- static/tickets.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/static/tickets.json b/static/tickets.json index 8218a3c..eb976e5 100644 --- a/static/tickets.json +++ b/static/tickets.json @@ -1,6 +1,5 @@ [ { - "type": "silver", "data": [ { "address": "0x002D3a2BfE09E3E29b6d38d58CaaD16EEe4C9BC5" @@ -106,7 +105,12 @@ { "owner": "0x891DF765C855E9848A18Ed18984B9f57cb3a4d47", "sig": "0x9701d7ec08583a80981c6e4e2aee0bef44236bb08f35d42511561ee992ed7bae0c049fa781aa49e089da5ec4a1c61d5fc8393a5b39a34e35ac9ec47642e5de191c" + }, + { + "owner": "0x1e5e12568b7103E8B22cd680A6fa6256DD66ED76", + "sig": "0x36e4f0cd2180d134c2e8ccd6afdf73966b2a7c4fd58e62d75f3d9963704c59b664ad2b014151ce468c904eec4c92f0d4ecb917bf4ab79e51655dd21a80f676a31c" } - ] + ], + "type": "silver" } ] \ No newline at end of file From 4b9d73a11addafb5e0f3c7c3dd717976c0dd1f40 Mon Sep 17 00:00:00 2001 From: Zachary Belford Date: Mon, 2 Dec 2024 11:18:00 -0800 Subject: [PATCH 11/12] remove consolelog --- src/server.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index fe360df..3755355 100644 --- a/src/server.ts +++ b/src/server.ts @@ -480,7 +480,6 @@ async function startServer(): Promise { server.log.error(err) process.exit(1) } - console.log(`Worker ${process.pid}: Archive-server is listening on http://0.0.0.0:${config.ARCHIVER_PORT}`) Logger.mainLogger.info(`Worker ${process.pid}: Archive-server is listening on http://0.0.0.0:${config.ARCHIVER_PORT}`) State.setActive() Collector.scheduleMissingTxsDataQuery() From 4cb814db96340d38394b91290f31968f68b7b4b5 Mon Sep 17 00:00:00 2001 From: Zachary Belford Date: Mon, 2 Dec 2024 12:38:47 -0800 Subject: [PATCH 12/12] Update src/Config.ts --- src/Config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.ts b/src/Config.ts index 9ef1716..68a7f64 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -242,7 +242,7 @@ let config: Config = { }, minSigRequired: 1, requiredSecurityLevel: 5 - } + }, maxRecordsPerRequest: 200, } // Override default config params from config file, env vars, and cli args