File size: 6,112 Bytes
93c7565 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | /**
* 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,
};
|