From 9ff185e124fce9165b6b58960a03634599dc0e18 Mon Sep 17 00:00:00 2001 From: Olamidepy Date: Mon, 1 Jun 2026 20:06:20 +0100 Subject: [PATCH 1/2] Feat: #302 [CONTRACT] Metadata hash standardization (CID strategy) + validation rules --- .../escrow/utils/metadata-hash.util.spec.ts | 18 ++++++++++++++++++ .../modules/escrow/utils/metadata-hash.util.ts | 18 ++++++++++++++++-- apps/frontend/lib/metadata-hash.test.ts | 18 ++++++++++++++++++ apps/frontend/lib/metadata-hash.ts | 18 ++++++++++++++++-- apps/onchain/src/lib.rs | 2 +- 5 files changed, 69 insertions(+), 5 deletions(-) diff --git a/apps/backend/src/modules/escrow/utils/metadata-hash.util.spec.ts b/apps/backend/src/modules/escrow/utils/metadata-hash.util.spec.ts index 6d9669e3..d90bf3ec 100644 --- a/apps/backend/src/modules/escrow/utils/metadata-hash.util.spec.ts +++ b/apps/backend/src/modules/escrow/utils/metadata-hash.util.spec.ts @@ -30,6 +30,24 @@ describe('normalizeMetadataHash', () => { expect(normalizeMetadataHash(cid)).toBe(digestHex); }); + it('rejects all-zero raw hex digests', () => { + expect(() => normalizeMetadataHash('0'.repeat(64))).toThrow('zeroes'); + }); + + it('rejects all-zero cid digests', () => { + const cid = `b${encodeBase32( + Uint8Array.from([0x01, 0x55, 0x12, 0x20, ...new Uint8Array(32)]), + )}`; + + expect(() => normalizeMetadataHash(cid)).toThrow('zeroes'); + }); + + it('rejects malformed raw hex digests', () => { + expect(() => normalizeMetadataHash(digestHex.slice(0, 62))).toThrow( + '64 hex characters', + ); + }); + it('rejects non-sha256 multihashes', () => { const cid = `b${encodeBase32( Uint8Array.from([0x01, 0x55, 0x13, 0x20, ...digest]), diff --git a/apps/backend/src/modules/escrow/utils/metadata-hash.util.ts b/apps/backend/src/modules/escrow/utils/metadata-hash.util.ts index 66eca8cb..8fcff776 100644 --- a/apps/backend/src/modules/escrow/utils/metadata-hash.util.ts +++ b/apps/backend/src/modules/escrow/utils/metadata-hash.util.ts @@ -6,13 +6,19 @@ const BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567'; const BASE58BTC_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; const HEX_32_RE = /^[0-9a-f]{64}$/; +const HEX_RE = /^[0-9a-f]+$/i; +const ZERO_DIGEST_HEX = '0'.repeat(SHA256_DIGEST_LENGTH * 2); export function normalizeMetadataHash(reference: string): string { const value = sanitizeReference(reference); const lowered = value.toLowerCase(); if (HEX_32_RE.test(lowered)) { - return lowered; + return validateDigestHex(lowered); + } + + if (HEX_RE.test(value)) { + throw new Error('metadata hash must be 64 hex characters'); } const cid = extractCid(value); @@ -20,7 +26,7 @@ export function normalizeMetadataHash(reference: string): string { ? extractDigestFromCidV0(cid) : extractDigestFromCidV1(cid); - return bytesToHex(digest); + return validateDigestHex(bytesToHex(digest)); } function sanitizeReference(reference: string): string { @@ -199,3 +205,11 @@ function bytesToHex(bytes: Uint8Array): string { '', ); } + +function validateDigestHex(digestHex: string): string { + if (digestHex === ZERO_DIGEST_HEX) { + throw new Error('metadata hash cannot be all zeroes'); + } + + return digestHex; +} diff --git a/apps/frontend/lib/metadata-hash.test.ts b/apps/frontend/lib/metadata-hash.test.ts index 72889392..e4c51685 100644 --- a/apps/frontend/lib/metadata-hash.test.ts +++ b/apps/frontend/lib/metadata-hash.test.ts @@ -30,6 +30,24 @@ describe('normalizeMetadataHash', () => { expect(normalizeMetadataHash(cid)).toBe(digestHex); }); + it('rejects all-zero raw hex digests', () => { + expect(() => normalizeMetadataHash('0'.repeat(64))).toThrow('zeroes'); + }); + + it('rejects all-zero cid digests', () => { + const cid = `b${encodeBase32( + Uint8Array.from([0x01, 0x55, 0x12, 0x20, ...new Uint8Array(32)]), + )}`; + + expect(() => normalizeMetadataHash(cid)).toThrow('zeroes'); + }); + + it('rejects malformed raw hex digests', () => { + expect(() => normalizeMetadataHash(digestHex.slice(0, 62))).toThrow( + '64 hex characters', + ); + }); + it('rejects non-sha256 multihashes', () => { const cid = `b${encodeBase32( Uint8Array.from([0x01, 0x55, 0x13, 0x20, ...digest]), diff --git a/apps/frontend/lib/metadata-hash.ts b/apps/frontend/lib/metadata-hash.ts index d792e330..a8ee7f49 100644 --- a/apps/frontend/lib/metadata-hash.ts +++ b/apps/frontend/lib/metadata-hash.ts @@ -6,13 +6,19 @@ const BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567'; const BASE58BTC_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; const HEX_32_RE = /^[0-9a-f]{64}$/; +const HEX_RE = /^[0-9a-f]+$/i; +const ZERO_DIGEST_HEX = '0'.repeat(SHA256_DIGEST_LENGTH * 2); export function normalizeMetadataHash(reference: string): string { const value = sanitizeReference(reference); const lowered = value.toLowerCase(); if (HEX_32_RE.test(lowered)) { - return lowered; + return validateDigestHex(lowered); + } + + if (HEX_RE.test(value)) { + throw new Error('metadata hash must be 64 hex characters'); } const cid = extractCid(value); @@ -20,7 +26,7 @@ export function normalizeMetadataHash(reference: string): string { ? extractDigestFromCidV0(cid) : extractDigestFromCidV1(cid); - return bytesToHex(digest); + return validateDigestHex(bytesToHex(digest)); } function sanitizeReference(reference: string): string { @@ -192,3 +198,11 @@ function bytesToHex(bytes: Uint8Array): string { '', ); } + +function validateDigestHex(digestHex: string): string { + if (digestHex === ZERO_DIGEST_HEX) { + throw new Error('metadata hash cannot be all zeroes'); + } + + return digestHex; +} diff --git a/apps/onchain/src/lib.rs b/apps/onchain/src/lib.rs index 27afb314..ee04e318 100644 --- a/apps/onchain/src/lib.rs +++ b/apps/onchain/src/lib.rs @@ -89,7 +89,7 @@ pub struct Escrow { pub threshold_amount: i128, // Threshold amount for multi-sig requirement pub required_signatures: u32, // Number of signatures required for release pub collected_signatures: Vec
, // Addresses that have signed for release - pub metadata_hash: BytesN<32>, // IPFS metadata hash for the escrow agreement + pub metadata_hash: BytesN<32>, // Raw SHA-256 digest bytes for escrow metadata } #[contracttype] From 4da096c9ca49655d027245d071842d13c243d9ac Mon Sep 17 00:00:00 2001 From: Olamidepy Date: Tue, 2 Jun 2026 15:31:46 +0100 Subject: [PATCH 2/2] Fix backend Soroban metadata hash fixture --- apps/backend/src/services/stellar/soroban-integration.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/services/stellar/soroban-integration.spec.ts b/apps/backend/src/services/stellar/soroban-integration.spec.ts index 6106b048..af9f2a76 100644 --- a/apps/backend/src/services/stellar/soroban-integration.spec.ts +++ b/apps/backend/src/services/stellar/soroban-integration.spec.ts @@ -34,7 +34,7 @@ describe('EscrowOperationsService Integration', () => { token, milestones, deadline, - '0'.repeat(64), // Valid hex metadata hash + '1'.repeat(64), // Valid non-zero hex metadata hash ); const op = ops[0] as any as {