Spaces:
Runtime error
Runtime error
File size: 6,221 Bytes
e92be04 | 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 | /**
* P2PCLAW Blockchain Registry Service
* =====================================
* Anchors research paper hashes to multiple EVM blockchains for
* permanent, immutable, trustless proof-of-existence.
*
* Storage method: 0-value transactions with JSON metadata in the data field.
* No smart contract needed — the transaction hash IS the proof.
*
* Supported chains (in priority order):
* 1. Polygon PoS mainnet — MATIC_RPC_URL + AGENT_PRIVATE_KEY (cheapest, ~$0.001)
* 2. Ethereum Sepolia — ETH_SEPOLIA_RPC + AGENT_PRIVATE_KEY (free testnet)
* 3. Base L2 mainnet — BASE_RPC_URL + AGENT_PRIVATE_KEY (Ethereum L2, ~$0.001)
*
* Environment variables:
* AGENT_PRIVATE_KEY or API_PRIVATE_KEY — EVM wallet private key (same for all chains)
* MATIC_RPC_URL — Polygon RPC (default: https://polygon-rpc.com/)
* ETH_SEPOLIA_RPC — Sepolia RPC (default: https://rpc.sepolia.org)
* BASE_RPC_URL — Base L2 RPC (default: https://mainnet.base.org)
*/
import { ethers } from 'ethers';
import crypto from 'crypto';
const PRIVATE_KEY = process.env.AGENT_PRIVATE_KEY || process.env.API_PRIVATE_KEY;
const CHAINS = [
{
id: 'polygon',
name: 'Polygon PoS',
rpc: process.env.MATIC_RPC_URL || 'https://polygon-rpc.com/',
enabled: !!PRIVATE_KEY,
},
{
id: 'sepolia',
name: 'Ethereum Sepolia',
rpc: process.env.ETH_SEPOLIA_RPC || 'https://rpc.sepolia.org',
enabled: !!PRIVATE_KEY,
},
{
id: 'base',
name: 'Base L2',
rpc: process.env.BASE_RPC_URL || 'https://mainnet.base.org',
enabled: !!PRIVATE_KEY,
},
];
// Wallet cache per chain
const _wallets = {};
async function getWallet(chain) {
if (_wallets[chain.id]) return _wallets[chain.id];
if (!PRIVATE_KEY) return null;
try {
const provider = new ethers.providers.JsonRpcProvider(chain.rpc);
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
_wallets[chain.id] = wallet;
return wallet;
} catch (e) {
console.error(`[BLOCKCHAIN] ❌ ${chain.name} wallet init failed: ${e.message}`);
return null;
}
}
/**
* Compute a SHA-256 content hash for a paper (deterministic, chain-agnostic).
*/
function contentHash(title, content, paperId) {
return crypto.createHash('sha256')
.update(`${paperId}:${title}:${content}`)
.digest('hex');
}
/**
* Anchor a paper to a single chain. Returns tx hash or null on failure.
*/
async function anchorToChain(chain, paperId, title, content, ipfsCid, authorId) {
const wallet = await getWallet(chain);
if (!wallet) return null;
const hash = contentHash(title, content, paperId);
const metadata = {
v: 2,
network: 'P2PCLAW',
paper_id: paperId,
title: title.slice(0, 120),
sha256: hash,
ipfs: ipfsCid || null,
author: authorId,
ts: Date.now(),
};
const hexData = ethers.utils.hexlify(
ethers.utils.toUtf8Bytes(JSON.stringify(metadata))
);
try {
const tx = await wallet.sendTransaction({
to: wallet.address,
value: 0,
data: hexData,
});
console.log(`[BLOCKCHAIN] ✅ ${chain.name} — paper ${paperId} → tx ${tx.hash}`);
return tx.hash;
} catch (e) {
// Log warning but do not throw — blockchain failure must never block paper publishing
console.warn(`[BLOCKCHAIN] ⚠️ ${chain.name} tx failed for ${paperId}: ${e.message}`);
return null;
}
}
/**
* Register a paper on all configured chains (fire-and-forget, non-blocking).
*
* @param {string} paperId — Internal paper ID
* @param {string} title — Paper title
* @param {string} content — Full paper content (used for hash)
* @param {string} ipfsCid — IPFS CID (optional)
* @param {string} authorId — Agent ID of the author
* @returns {Object} { polygon, sepolia, base, sha256 } — tx hashes per chain (null if skipped)
*/
export async function registerPaperOnChain(paperId, title, content, ipfsCid, authorId) {
if (!PRIVATE_KEY) {
console.log('[BLOCKCHAIN] ℹ️ No wallet key set (AGENT_PRIVATE_KEY). Blockchain anchoring disabled.');
return null;
}
const hash = contentHash(title, content || '', paperId);
console.log(`[BLOCKCHAIN] 📝 Anchoring paper ${paperId} (sha256=${hash.slice(0, 16)}…)`);
// Run all chains in parallel; failures are isolated
const results = await Promise.allSettled(
CHAINS.filter(c => c.enabled).map(chain =>
anchorToChain(chain, paperId, title, content, ipfsCid, authorId)
)
);
const txMap = {};
CHAINS.filter(c => c.enabled).forEach((chain, i) => {
txMap[chain.id] = results[i].status === 'fulfilled' ? results[i].value : null;
});
const successCount = Object.values(txMap).filter(Boolean).length;
console.log(`[BLOCKCHAIN] ${successCount}/${CHAINS.filter(c=>c.enabled).length} chains anchored. sha256=${hash}`);
return { ...txMap, sha256: hash };
}
/**
* Backwards-compatible alias (old signature: title, arweaveTxId, leanProofHash, authorId)
* Used by consensusService.js's existing call site.
*/
export async function registerPaperOnChainLegacy(title, arweaveTxId, leanProofHash, authorId) {
// Old call site doesn't have paperId/content — stub with available data
const paperId = `legacy-${Date.now()}`;
return registerPaperOnChain(paperId, title, leanProofHash || '', arweaveTxId, authorId);
}
// Init log
if (PRIVATE_KEY) {
console.log('[BLOCKCHAIN] 🔗 Wallet key found — multi-chain anchoring enabled (Polygon + Sepolia + Base)');
// Log wallet address once
const provider = new ethers.providers.JsonRpcProvider(CHAINS[0].rpc);
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
console.log(`[BLOCKCHAIN] 🔑 Wallet address: ${wallet.address}`);
_wallets['polygon'] = wallet;
} else {
console.log('[BLOCKCHAIN] ℹ️ Set AGENT_PRIVATE_KEY on Railway to enable multi-chain paper anchoring.');
}
|