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.');
}