divhanimajokweni-ctrl's picture
feat: submit ProofBridge Liner to LabLab AI AMD Developer Hackathon
93c7565
Raw History Blame Contribute Delete
6.11 kB
/**
* prover/submitter.js
* ----------------------------------------------------------
* Relays fetcher results into ProofBridge Liner actions.
*
* Reads .local/state/prover-state.json (produced by fetcher.js)
* and, for every asset with status "fresh", plans
* CircuitBreaker.updateProof(assetId, deedHash). For any asset
* with status "mismatch" or "unreachable", plans tripCircuit(reason).
*
* The submitter now produces SafeKrypte-backed action attestations
* before broadcast. This replaces unsafe local hot-wallet signing in
* the MVP path while keeping contract broadcasting isolated until the
* CircuitBreaker address and ABI are wired.
*/
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const { ethers } = require('ethers');
const notifier = require('./notifier');
const STATE_PATH = path.resolve(__dirname, '..', '.local', 'state', 'prover-state.json');
const ATTESTATION_PATH = path.resolve(__dirname, '..', '.local', 'state', 'submitter-attestations.json');
function stableJson(value) {
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
if (value && typeof value === 'object') {
return `{${Object.keys(value)
.sort()
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
.join(',')}}`;
}
return JSON.stringify(value);
}
function digestPayload(payload) {
return `0x${crypto.createHash('sha256').update(stableJson(payload)).digest('hex')}`;
}
function planActions(state) {
const { scoreAsset } = require('./scorer');
const actions = [];
for (const r of state.results) {
const score = scoreAsset(r.gatewayResults, r.expectedHash);
console.log(`[submitter] Asset ${r.assetId}: scenario ${score.scenario}, score ${score.triggerScore.toFixed(3)}, threshold ${score.thresholdUsed}, trip ${score.trip}`);
if (score.trip) {
actions.push({
kind: 'tripCircuit',
assetId: r.assetId,
reason: `Stratified trigger: scenario ${score.scenario}, score ${score.triggerScore.toFixed(3)} ≥ ${score.thresholdUsed}`,
});
} else if (r.status === 'fresh') {
actions.push({
kind: 'updateProof',
assetId: r.assetId,
deedHash: '0x' + r.actualHash,
});
} else if (r.status === 'unreachable_threshold') {
actions.push({
kind: 'tripCircuit',
assetId: r.assetId,
reason: `Document unreachable after ${r.consecutiveUnreachable} consecutive polls`,
});
}
}
return actions;
}
async function requestThresholdSignature(digest) {
const { collectSigs, aggregate } = require('./tss-signer');
const sigs = await collectSigs(digest);
const aggregated = aggregate(sigs);
return {
signature: aggregated,
signerAddresses: sigs.map(s => s.signer),
keyId: 'threshold-quorum',
auditId: null,
};
}
async function attestActions(actions, state) {
const attestations = [];
for (const action of actions) {
const payload = {
type: 'ProofBridgeLinerAction',
action,
stateRunId: state.runId || null,
stateGeneratedAt: state.generatedAt || null,
issuedAt: new Date().toISOString(),
};
// For V2, use threshold digest
let digest;
if (action.kind === 'updateProof') {
// actionDigest(assetId, deedHash) = keccak256(abi.encodePacked(assetId, deedHash, block.chainid, address(this)))
const contractAddress = process.env.CIRCUIT_BREAKER_ADDRESS || '0x1234567890123456789012345678901234567890';
const chainId = 80002; // Polygon Amoy
digest = ethers.keccak256(ethers.concat([action.assetId, action.deedHash, ethers.toBeHex(chainId, 32), contractAddress]));
} else if (action.kind === 'tripCircuit') {
const contractAddress = process.env.CIRCUIT_BREAKER_ADDRESS || '0x1234567890123456789012345678901234567890';
const chainId = 80002;
digest = ethers.keccak256(ethers.concat([ethers.toUtf8Bytes(action.reason), ethers.toBeHex(chainId, 32), contractAddress]));
}
const signed = await requestThresholdSignature(digest);
attestations.push({
...payload,
digest,
...signed,
backend: 'threshold-quorum',
});
}
return attestations;
}
async function main() {
const dryRun = process.argv.includes('--dry-run') || process.argv.includes('--dry');
if (!fs.existsSync(STATE_PATH)) {
console.error('[submitter] no prover state found; run fetcher.js first.');
process.exit(1);
}
// Notify start
try { notifier.submitStart({}); } catch (_) {}
const state = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
const actions = planActions(state);
console.log(`[submitter] planning ${actions.length} on-chain action(s)`);
for (const a of actions) {
if (a.kind === 'updateProof') {
console.log(` -> updateProof(${a.assetId}, ${a.deedHash})`);
} else {
console.log(` -> tripCircuit(${a.assetId}, "${a.reason}")`);
}
}
const attestations = await attestActions(actions, state);
fs.mkdirSync(path.dirname(ATTESTATION_PATH), { recursive: true });
fs.writeFileSync(ATTESTATION_PATH, `${JSON.stringify({ attestations }, null, 2)}\n`);
console.log(`[submitter] wrote ${attestations.length} SafeKrypte attestation(s) to ${ATTESTATION_PATH}`);
// Notify completion
try {
notifier.submitComplete({
count: attestations.length,
actions: attestations.map(a => a.action.kind),
dryRun,
});
} catch (_) {}
if (dryRun || !process.env.CIRCUIT_BREAKER_ADDRESS) {
console.log('[submitter] dry-run complete. Set CIRCUIT_BREAKER_ADDRESS to enable broadcast wiring.');
return;
}
console.log('[submitter] broadcast not yet enabled; SafeKrypte attestations generated successfully.');
}
if (require.main === module) {
main().catch((error) => {
try { notifier.submitError({ error: error.message, stack: error.stack }); } catch (_) {}
console.error(`[submitter] ${error.message}`);
process.exit(1);
});
}
module.exports = {
attestActions,
digestPayload,
planActions,
requestThresholdSignature,
stableJson,
};