defarm-sdk
Version:
DeFarm SDK - On-premise blockchain data processing and tokenization engine for agriculture supply chain
534 lines (452 loc) • 14 kB
JavaScript
/**
* 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 };