UNPKG

defarm-sdk

Version:

DeFarm SDK - On-premise blockchain data processing and tokenization engine for agriculture supply chain

534 lines (452 loc) 14 kB
/** * Provenance Engine - Tracks complete asset history and chain of custody * Works with isolated client database */ const crypto = require('crypto'); class ProvenanceEngine { constructor(config = {}) { this.config = { enableBlockchain: config.enableBlockchain !== false, strictMode: config.strictMode !== false, hashAlgorithm: config.hashAlgorithm || 'sha256', ...config }; this.dbManager = config.database; this.blockchainEngine = config.blockchain; // In-memory cache for performance this.provenanceCache = new Map(); this.hashChain = new Map(); } /** * Initialize provenance tables in client's isolated database */ async initializeDatabase() { if (!this.dbManager) { console.warn('No database configured for provenance tracking'); return; } // Create provenance tables in client's database const tables = [ `CREATE TABLE IF NOT EXISTS provenance_records ( id SERIAL PRIMARY KEY, asset_id VARCHAR(255) NOT NULL, event_type VARCHAR(100) NOT NULL, event_data JSONB NOT NULL, actor VARCHAR(255), location JSONB, timestamp BIGINT NOT NULL, previous_hash VARCHAR(255), current_hash VARCHAR(255) NOT NULL, blockchain_tx VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(current_hash) )`, `CREATE TABLE IF NOT EXISTS provenance_chain ( id SERIAL PRIMARY KEY, asset_id VARCHAR(255) NOT NULL, chain_hash VARCHAR(255) NOT NULL, record_count INTEGER DEFAULT 0, first_record_id INTEGER, last_record_id INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(asset_id) )`, `CREATE TABLE IF NOT EXISTS provenance_verification ( id SERIAL PRIMARY KEY, asset_id VARCHAR(255) NOT NULL, verification_hash VARCHAR(255) NOT NULL, verification_type VARCHAR(50), verified_by VARCHAR(255), verified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, result JSONB, UNIQUE(verification_hash) )` ]; // Create indexes for performance const indexes = [ 'CREATE INDEX IF NOT EXISTS idx_provenance_asset ON provenance_records(asset_id)', 'CREATE INDEX IF NOT EXISTS idx_provenance_timestamp ON provenance_records(timestamp)', 'CREATE INDEX IF NOT EXISTS idx_provenance_event ON provenance_records(event_type)', 'CREATE INDEX IF NOT EXISTS idx_provenance_hash ON provenance_records(current_hash)' ]; try { for (const table of tables) { await this.dbManager.execute(table); } for (const index of indexes) { await this.dbManager.execute(index); } console.log('✅ Provenance tables initialized in client database'); } catch (error) { console.error('Failed to initialize provenance tables:', error); throw error; } } /** * Record provenance event */ async recordEvent(assetId, eventType, eventData, options = {}) { const timestamp = options.timestamp || Date.now(); // Get previous hash for chain continuity const previousHash = await this.getPreviousHash(assetId); // Create provenance record const record = { asset_id: assetId, event_type: eventType, event_data: eventData, actor: options.actor || 'system', location: options.location || null, timestamp, previous_hash: previousHash, current_hash: null }; // Calculate hash record.current_hash = this.calculateHash(record); // Store in database if (this.dbManager) { try { const result = await this.dbManager.storeEvent(record); record.id = result.id; // Update chain await this.updateChain(assetId, record); } catch (error) { console.error('Failed to store provenance record:', error); throw error; } } // Store on blockchain if enabled if (this.config.enableBlockchain && this.blockchainEngine) { try { const txHash = await this.blockchainEngine.recordProvenance(record); record.blockchain_tx = txHash; // Update record with blockchain tx if (this.dbManager) { await this.dbManager.update( 'provenance_records', { blockchain_tx: txHash }, { id: record.id } ); } } catch (error) { console.warn('Blockchain recording failed:', error); } } // Update cache this.updateCache(assetId, record); return record; } /** * Get complete provenance chain for asset */ async getProvenanceChain(assetId, options = {}) { // Check cache first if (this.provenanceCache.has(assetId) && !options.skipCache) { return this.provenanceCache.get(assetId); } if (!this.dbManager) { return []; } try { const records = await this.dbManager.query( 'SELECT * FROM provenance_records WHERE asset_id = $1 ORDER BY timestamp ASC', [assetId] ); // Verify chain integrity if (this.config.strictMode) { const isValid = await this.verifyChain(records); if (!isValid) { console.warn(`Chain integrity violation detected for asset ${assetId}`); } } // Update cache this.provenanceCache.set(assetId, records); return records; } catch (error) { console.error('Failed to retrieve provenance chain:', error); return []; } } /** * Verify provenance chain integrity */ async verifyChain(records) { if (!records || records.length === 0) { return true; } let previousHash = null; for (const record of records) { // Check hash continuity if (record.previous_hash !== previousHash) { console.error(`Hash chain broken at record ${record.id}`); return false; } // Recalculate and verify hash const calculatedHash = this.calculateHash({ asset_id: record.asset_id, event_type: record.event_type, event_data: record.event_data, actor: record.actor, location: record.location, timestamp: record.timestamp, previous_hash: record.previous_hash }); if (calculatedHash !== record.current_hash) { console.error(`Hash mismatch at record ${record.id}`); return false; } previousHash = record.current_hash; } return true; } /** * Get provenance summary */ async getProvenanceSummary(assetId) { const chain = await this.getProvenanceChain(assetId); if (chain.length === 0) { return null; } const summary = { asset_id: assetId, total_events: chain.length, first_event: chain[0], last_event: chain[chain.length - 1], event_types: {}, actors: new Set(), locations: new Set(), time_span: { start: chain[0].timestamp, end: chain[chain.length - 1].timestamp, duration_days: Math.floor((chain[chain.length - 1].timestamp - chain[0].timestamp) / 86400000) }, chain_valid: await this.verifyChain(chain) }; // Aggregate event types for (const record of chain) { summary.event_types[record.event_type] = (summary.event_types[record.event_type] || 0) + 1; summary.actors.add(record.actor); if (record.location) { summary.locations.add(JSON.stringify(record.location)); } } summary.actors = Array.from(summary.actors); summary.locations = Array.from(summary.locations).map(l => JSON.parse(l)); return summary; } /** * Search provenance records */ async searchProvenance(criteria) { if (!this.dbManager) { return []; } let query = 'SELECT * FROM provenance_records WHERE 1=1'; const params = []; let paramIndex = 1; if (criteria.asset_id) { query += ` AND asset_id = $${paramIndex++}`; params.push(criteria.asset_id); } if (criteria.event_type) { query += ` AND event_type = $${paramIndex++}`; params.push(criteria.event_type); } if (criteria.actor) { query += ` AND actor = $${paramIndex++}`; params.push(criteria.actor); } if (criteria.start_date) { query += ` AND timestamp >= $${paramIndex++}`; params.push(criteria.start_date); } if (criteria.end_date) { query += ` AND timestamp <= $${paramIndex++}`; params.push(criteria.end_date); } query += ' ORDER BY timestamp DESC'; if (criteria.limit) { query += ` LIMIT ${criteria.limit}`; } try { return await this.dbManager.query(query, params); } catch (error) { console.error('Provenance search failed:', error); return []; } } /** * Export provenance data */ async exportProvenance(assetId, format = 'json') { const chain = await this.getProvenanceChain(assetId); switch (format) { case 'json': return JSON.stringify(chain, null, 2); case 'csv': return this.convertToCSV(chain); case 'blockchain': return await this.exportToBlockchain(chain); default: throw new Error(`Unsupported format: ${format}`); } } // Private helper methods async getPreviousHash(assetId) { if (this.hashChain.has(assetId)) { return this.hashChain.get(assetId); } if (!this.dbManager) { return null; } try { const result = await this.dbManager.query( 'SELECT current_hash FROM provenance_records WHERE asset_id = $1 ORDER BY timestamp DESC LIMIT 1', [assetId] ); const hash = result[0]?.current_hash || null; this.hashChain.set(assetId, hash); return hash; } catch (error) { return null; } } async updateChain(assetId, record) { if (!this.dbManager) { return; } try { const existing = await this.dbManager.query( 'SELECT * FROM provenance_chain WHERE asset_id = $1', [assetId] ); if (existing.length === 0) { // Create new chain await this.dbManager.insert('provenance_chain', { asset_id: assetId, chain_hash: record.current_hash, record_count: 1, first_record_id: record.id, last_record_id: record.id }); } else { // Update existing chain await this.dbManager.update( 'provenance_chain', { chain_hash: record.current_hash, record_count: existing[0].record_count + 1, last_record_id: record.id, updated_at: new Date() }, { asset_id: assetId } ); } } catch (error) { console.error('Failed to update chain:', error); } } updateCache(assetId, record) { if (!this.provenanceCache.has(assetId)) { this.provenanceCache.set(assetId, []); } const cache = this.provenanceCache.get(assetId); cache.push(record); // Update hash chain this.hashChain.set(assetId, record.current_hash); // Limit cache size if (this.provenanceCache.size > 1000) { const firstKey = this.provenanceCache.keys().next().value; this.provenanceCache.delete(firstKey); } } calculateHash(data) { const hash = crypto.createHash(this.config.hashAlgorithm); // Create deterministic string representation const hashInput = [ data.asset_id, data.event_type, JSON.stringify(data.event_data), data.actor, JSON.stringify(data.location), data.timestamp, data.previous_hash || '' ].join('|'); hash.update(hashInput); return hash.digest('hex'); } convertToCSV(records) { if (!records || records.length === 0) { return ''; } const headers = [ 'ID', 'Asset ID', 'Event Type', 'Actor', 'Timestamp', 'Previous Hash', 'Current Hash' ]; const rows = records.map(r => [ r.id, r.asset_id, r.event_type, r.actor, new Date(r.timestamp).toISOString(), r.previous_hash || '', r.current_hash ]); return [ headers.join(','), ...rows.map(row => row.join(',')) ].join('\n'); } async exportToBlockchain(records) { if (!this.blockchainEngine) { throw new Error('Blockchain engine not configured'); } const results = []; for (const record of records) { if (!record.blockchain_tx) { try { const tx = await this.blockchainEngine.recordProvenance(record); results.push({ record_id: record.id, tx_hash: tx }); } catch (error) { results.push({ record_id: record.id, error: error.message }); } } else { results.push({ record_id: record.id, tx_hash: record.blockchain_tx }); } } return results; } /** * Get statistics */ async getStats() { if (!this.dbManager) { return { cacheSize: this.provenanceCache.size, hashChainSize: this.hashChain.size }; } try { const stats = await this.dbManager.query(` SELECT COUNT(DISTINCT asset_id) as total_assets, COUNT(*) as total_records, COUNT(DISTINCT event_type) as event_types, COUNT(DISTINCT actor) as unique_actors FROM provenance_records `); return { ...stats[0], cacheSize: this.provenanceCache.size, hashChainSize: this.hashChain.size }; } catch (error) { return { error: error.message, cacheSize: this.provenanceCache.size, hashChainSize: this.hashChain.size }; } } } module.exports = { ProvenanceEngine };