From 275cb4969d9557a82690a4dfcecaee4ca97a14a9 Mon Sep 17 00:00:00 2001 From: derekwalter999 Date: Fri, 31 Jul 2026 10:56:02 +0100 Subject: [PATCH 1/2] test: Horizon duplicate-hash chaos with idempotency --- .../chaos/horizonDuplicateHash.test.ts | 144 ++++++++++++++++++ src/services/stellarSubmissionService.ts | 8 +- 2 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/chaos/horizonDuplicateHash.test.ts diff --git a/src/__tests__/chaos/horizonDuplicateHash.test.ts b/src/__tests__/chaos/horizonDuplicateHash.test.ts new file mode 100644 index 00000000..a50d8a81 --- /dev/null +++ b/src/__tests__/chaos/horizonDuplicateHash.test.ts @@ -0,0 +1,144 @@ +import * as StellarSdk from '@stellar/stellar-sdk'; +import { StellarSubmissionService } from '../../services/stellarSubmissionService'; +import { globalMetrics } from '../../lib/metrics'; + +// Mock logger +jest.mock('../../lib/logger', () => ({ + globalLogger: { + child: jest.fn().mockReturnValue({ + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }), + }, + logger: { + child: jest.fn().mockReturnValue({ + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }), + } +})); + +// Mock environment +jest.mock('../../config/env', () => ({ + env: { + STELLAR_NETWORK: 'testnet', + STELLAR_NETWORK_PASSPHRASE: 'Test SDF Network ; September 2015', + STELLAR_SERVER_SECRET: 'SABERIntegrationTestSecretKey1234567890ABCDEF', + }, +})); + +describe('Horizon Duplicate Hash Chaos Tests', () => { + let service: StellarSubmissionService; + let mockServer: any; + let metricsIncrementSpy: jest.SpyInstance; + + beforeEach(() => { + process.env.STELLAR_SERVER_SECRET = 'SABERIntegrationTestSecretKey1234567890ABCDEF'; + jest.clearAllMocks(); + + metricsIncrementSpy = jest.spyOn(globalMetrics, 'increment'); + + mockServer = { + getAccount: jest.fn().mockResolvedValue({ + accountId: () => 'G-MOCK-PUBLIC-KEY', + sequenceNumber: () => '1', + incrementSequenceNumber: jest.fn(), + }), + sendTransaction: jest.fn(), + }; + + StellarSdk.rpc.Server = jest.fn(() => mockServer) as any; + StellarSdk.Keypair.fromSecret = jest.fn(() => ({ + publicKey: () => 'G-MOCK-PUBLIC-KEY', + sign: jest.fn(), + })) as any; + + StellarSdk.Asset.native = jest.fn(() => ({ code: 'XLM', issuer: undefined })) as any; + + StellarSdk.TransactionBuilder = jest.fn(() => ({ + addOperation: jest.fn().mockReturnThis(), + setTimeout: jest.fn().mockReturnThis(), + build: jest.fn().mockReturnValue({ + hash: () => Buffer.from('mock-hash'), + sign: jest.fn(), + }), + })) as any; + + StellarSdk.Operation.payment = jest.fn() as any; + (StellarSdk as any).BASE_FEE = '100'; + + service = new StellarSubmissionService(); + }); + + afterEach(() => { + metricsIncrementSpy.mockRestore(); + }); + + it('should treat first-attempt duplicate as success (retry recovery scenario)', async () => { + mockServer.sendTransaction.mockResolvedValueOnce({ + hash: 'mock-hash', + status: 'DUPLICATE', + latestLedger: 12345, + latestLedgerCloseTime: 1234567890, + }); + + const result = await service.submitPayment('G-DEST', '10.0'); + + expect(result.status).toBe('DUPLICATE'); + expect(metricsIncrementSpy).toHaveBeenCalledWith('submission.duplicate.recovered', 1); + }); + + it('should treat true-duplicate as success without double persisting (client bug)', async () => { + mockServer.sendTransaction.mockResolvedValueOnce({ + hash: 'mock-hash', + status: 'DUPLICATE', + latestLedger: 12345, + latestLedgerCloseTime: 1234567890, + }); + + const result = await service.submitPayment('G-DEST', '10.0'); + + expect(result.status).toBe('DUPLICATE'); + expect(metricsIncrementSpy).toHaveBeenCalledWith('submission.duplicate.recovered', 1); + + expect(result).toBeDefined(); + + expect(service.getTransactionCacheSize()).toBe(1); + }); + + it('Concurrent duplicate submissions from two workers coalesce', async () => { + mockServer.sendTransaction + .mockResolvedValueOnce({ + hash: 'mock-hash', + status: 'PENDING', + latestLedger: 12345, + latestLedgerCloseTime: 1234567890, + }) + .mockResolvedValueOnce({ + hash: 'mock-hash', + status: 'DUPLICATE', + latestLedger: 12345, + latestLedgerCloseTime: 1234567890, + }); + + const worker1 = service; + const worker2 = new StellarSubmissionService(); + + const [res1, res2] = await Promise.all([ + worker1.submitPayment('G-DEST', '10.0'), + worker2.submitPayment('G-DEST', '10.0'), + ]); + + expect(res1.status).toBe('PENDING'); + expect(res2.status).toBe('DUPLICATE'); + + expect(metricsIncrementSpy).toHaveBeenCalledWith('submission.duplicate.recovered', 1); + + expect(res1).toBeDefined(); + expect(res2).toBeDefined(); + }); +}); diff --git a/src/services/stellarSubmissionService.ts b/src/services/stellarSubmissionService.ts index bd17d639..56ba1d7f 100644 --- a/src/services/stellarSubmissionService.ts +++ b/src/services/stellarSubmissionService.ts @@ -10,6 +10,7 @@ import { shouldRetryStellarRPCFailure, createStellarErrorResponse } from '../lib/stellarRpcFailure'; +import { globalMetrics } from '../lib/metrics'; const logger = globalLogger.child({ service: 'stellar-submission' }); @@ -265,10 +266,13 @@ export class StellarSubmissionService { if (result.status === 'PENDING') { return result; } else if (result.status === 'DUPLICATE') { - throw Errors.conflict('Transaction already submitted', { - hash: result.hash, + globalMetrics.increment('submission.duplicate.recovered', 1); + logger.info('Recovered duplicate transaction submission', { transactionHash, + attemptCount, + operation: 'send_transaction', }); + return result; } else if (result.status === 'TRY_AGAIN_LATER') { throw Errors.serviceUnavailable('Transaction rate limited, try again later'); } else { From a9183321bdcf717f1f8c3a08109d35522e6c3cf2 Mon Sep 17 00:00:00 2001 From: derekwalter999 Date: Fri, 31 Jul 2026 11:06:54 +0100 Subject: [PATCH 2/2] feat: SLSA attestation verification for contract upgrades --- src/security/attestationVerifier.test.ts | 117 +++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/security/attestationVerifier.test.ts diff --git a/src/security/attestationVerifier.test.ts b/src/security/attestationVerifier.test.ts new file mode 100644 index 00000000..5de9b5a2 --- /dev/null +++ b/src/security/attestationVerifier.test.ts @@ -0,0 +1,117 @@ +import { verifyReproducibleBuildAttestation } from './attestationVerifier'; + +describe('attestationVerifier', () => { + const allowedBuilderIds = ['https://github.com/RevoraOrg/builder', 'https://github.com/trusted/builder']; + const targetCodeId = 'abc123def456'; + + it('should verify a valid attestation with matching builder and digest', () => { + const validAttestation = { + builder: { id: 'https://github.com/RevoraOrg/builder' }, + predicateType: 'https://slsa.dev/provenance/v0.2', + subject: [ + { + name: 'contract.wasm', + digest: { sha256: targetCodeId } + } + ] + }; + + const result = verifyReproducibleBuildAttestation(validAttestation, targetCodeId, allowedBuilderIds); + expect(result.builderId).toBe('https://github.com/RevoraOrg/builder'); + expect(result.subjectDigest).toBe(targetCodeId); + }); + + it('should reject attestation from unknown builder', () => { + const unknownBuilderAttestation = { + builder: { id: 'https://github.com/malicious/builder' }, + predicateType: 'https://slsa.dev/provenance/v0.2', + subject: [ + { + name: 'contract.wasm', + digest: { sha256: targetCodeId } + } + ] + }; + + expect(() => { + verifyReproducibleBuildAttestation(unknownBuilderAttestation, targetCodeId, allowedBuilderIds); + }).toThrow('Attestation builder identity is not authorized for tenant'); + }); + + it('should reject attestation missing builder id', () => { + const missingBuilderAttestation = { + predicateType: 'https://slsa.dev/provenance/v0.2', + subject: [ + { + name: 'contract.wasm', + digest: { sha256: targetCodeId } + } + ] + }; + + expect(() => { + verifyReproducibleBuildAttestation(missingBuilderAttestation, targetCodeId, allowedBuilderIds); + }).toThrow('Attestation missing builder.id'); + }); + + it('should reject attestation not matching target code id', () => { + const mismatchAttestation = { + builder: { id: 'https://github.com/RevoraOrg/builder' }, + predicateType: 'https://slsa.dev/provenance/v0.2', + subject: [ + { + name: 'contract.wasm', + digest: { sha256: 'someotherdigest' } + } + ] + }; + + expect(() => { + verifyReproducibleBuildAttestation(mismatchAttestation, targetCodeId, allowedBuilderIds); + }).toThrow('Attestation subject payload does not contain a matching target code identifier'); + }); + + it('should accept when name matches target code id but digest does not', () => { + const nameMatchAttestation = { + builder: { id: 'https://github.com/RevoraOrg/builder' }, + predicateType: 'https://slsa.dev/provenance/v0.2', + subject: [ + { + name: targetCodeId, + digest: { sha256: 'someotherdigest' } + } + ] + }; + + const result = verifyReproducibleBuildAttestation(nameMatchAttestation, targetCodeId, allowedBuilderIds); + expect(result.builderId).toBe('https://github.com/RevoraOrg/builder'); + expect(result.subjectName).toBe(targetCodeId); + }); + + it('should reject unsupported predicate type', () => { + const badPredicateAttestation = { + builder: { id: 'https://github.com/RevoraOrg/builder' }, + predicateType: 'https://slsa.dev/provenance/v1.0', // unsupported + subject: [ + { + name: 'contract.wasm', + digest: { sha256: targetCodeId } + } + ] + }; + + expect(() => { + verifyReproducibleBuildAttestation(badPredicateAttestation, targetCodeId, allowedBuilderIds); + }).toThrow('Unsupported attestation predicate type'); + }); + + it('should reject if attestation is not an object', () => { + expect(() => { + verifyReproducibleBuildAttestation(null, targetCodeId, allowedBuilderIds); + }).toThrow('Attestation must be an object'); + + expect(() => { + verifyReproducibleBuildAttestation('string_attestation', targetCodeId, allowedBuilderIds); + }).toThrow('Attestation must be an object'); + }); +});