Files
apxtri/models/Blockchain.js
T

520 lines
20 KiB
JavaScript
Executable File

const fs = require("fs-extra");
const path = require("path");
const openpgp = require("openpgp");
const crypto = require("crypto");
const zlib = require('zlib');
const Odmdb = require('./Odmdb.js');
const axios = require("axios");
const Nations = require("./Nations.js");
const DATA_PATH = process.env.DATAPATH;
const TRIBE_PATH = path.join(DATA_PATH, process.env.TOWN || 'apxtri');
const PRIVKMAYOR = process.env.PRIVKMAYOR;
let pendingTransactions = [];
let blockchain = []; // This will hold the chain in memory
let receivedBlocks = {};
let proposedBlock = null;
let consensusVotes = {};
const log = (message, ...args) => console.log(`[Blockchain] ${message}`, ...args);
/**
* Main class for handling blockchain operations.
*/
class Blockchain {
/**
* Calculates the SHA-256 hash of a block's content.
*/
static _calculateHash(block) {
const blockString = JSON.stringify({
index: block.index,
timestamp: block.timestamp,
transactions: block.transactions,
previousHash: block.previousHash,
validator: block.validator
});
return crypto.createHash('sha256').update(blockString).digest('hex');
}
/**
* Verifies the integrity and signature of an incoming block.
*/
static async _verifySignature(block) {
try {
if (block.hash !== this._calculateHash(block)) {
log(`Verification failed: Hash mismatch for block #${block.index}`);
return false;
}
const townsDb = new Odmdb('towns', process.env.TOWN || 'apxtri');
const validatorTownResult = await townsDb.read(block.validator, { xprofils: ['admin'] });
if (validatorTownResult.status !== 200 || !validatorTownResult.data.pubkey) {
log(`Verification failed: Public key for validator ${block.validator} not found.`);
return false;
}
const publicKey = await openpgp.readKey({ armoredKey: validatorTownResult.data.pubkey });
const message = await openpgp.createMessage({ text: JSON.stringify(block) });
const verificationResult = await openpgp.verify({
message,
signature: await openpgp.readSignature({ armoredSignature: block.signature }),
verificationKeys: publicKey
});
await verificationResult.signatures[0].verified;
log(`Signature verified for block #${block.index} from ${block.validator}`);
return true;
} catch (error) {
log(`Block verification failed for block #${block.index}:`, error.message);
return false;
}
}
/**
* Signs a given data object with the mayor's private key.
*/
static async _sign(dataToSign) {
if (!PRIVKMAYOR) throw new Error("PRIVKMAYOR private key is not defined in .env");
let privateKey = await openpgp.readPrivateKey({ armoredKey: PRIVKMAYOR });
const passphrase = process.env.PRIVKPASSPHRASE;
if (passphrase) {
privateKey = await openpgp.decryptKey({ privateKey, passphrase });
}
const message = await openpgp.createMessage({ text: JSON.stringify(dataToSign) });
return openpgp.sign({ message, signingKeys: privateKey });
}
/**
* Adds a new transaction to the pending pool.
*/
static createTransaction(type, model, payload) {
log(`New transaction for ${model}: ${type}`);
pendingTransactions.push({ id: crypto.randomUUID(), type, model, payload });
}
/**
* Creates a new block from pending transactions and broadcasts it.
*/
static async createBlock() {
if (pendingTransactions.length === 0) return;
log("Attempting to create new block...");
const previousHash = blockchain.length > 0 ? blockchain[blockchain.length - 1].hash : "0".repeat(64);
let block = {
index: blockchain.length,
timestamp: Date.now(),
transactions: [...pendingTransactions],
previousHash,
validator: process.env.TOWN,
};
block.hash = this._calculateHash(block);
try {
block.signature = await this._sign(block);
receivedBlocks[block.validator] = block;
pendingTransactions = [];
log(`✅ New block #${block.index} created successfully. Hash: ${block.hash}`);
await this._broadcast('/receive', block);
} catch (error) {
log("❌ Failed to sign and create new block:", error);
}
}
/**
* Generic broadcast function to send data to all peers.
*/
static async _broadcast(endpoint, payload) {
log(`Broadcasting to ${endpoint}...`);
const townsDb = new Odmdb('towns', process.env.TOWN || 'apxtri');
let allTowns = [];
try { allTowns = await fs.readJson(path.join(townsDb.objPath, 'idx', 'lst_townId.json')); } catch { allTowns = []; }
if (!Array.isArray(allTowns)) allTowns = [];
for (const townId of allTowns) {
if (townId === process.env.TOWN) continue;
try {
const t = await townsDb.read(townId, { xprofils: ['admin'] });
if (t.status !== 200 || !t.data || !t.data.dns || !t.data.dns[0]) continue;
const peerUrl = `http://${t.data.dns[0]}/apxtri/blockchain${endpoint}`;
try {
await axios.post(peerUrl, payload, { timeout: 5000 });
} catch (error) {
log(`Failed to send to ${townId} at ${peerUrl}.`, error.message);
}
} catch {}
}
}
/**
* Handles a block received from a peer.
*/
static async receiveBlock(block) {
log(`Received block from ${block.validator}`);
if (!await this._verifySignature(block) || receivedBlocks[block.validator]) {
log(`Discarding invalid or duplicate block from ${block.validator}.`);
return;
}
receivedBlocks[block.validator] = block;
log(`Accepted and stored block from ${block.validator}.`);
}
/**
* Applies the transactions from a finalized block to the local database.
*/
static async updateLocalData(block) {
log(`Updating local data with transactions from block #${block.index}`);
const role = { xprofils: ['admin'], xalias: 'blockchain' };
for (const tx of block.transactions) {
const { type, model, payload } = tx;
log(`Applying ${type} to ${model} with ID ${payload.id || payload.townId}`);
try {
const db = new Odmdb(model, process.env.TOWN || 'apxtri');
await db._loadSchema();
const actionType = type.toLowerCase();
let result;
if (actionType === 'create') {
result = await db.create(payload, role);
} else if (actionType === 'update') {
result = await db.update(payload, role);
} else if (actionType === 'delete') {
const keyid = payload[db.schema.apxid] || payload.id || payload.townId;
result = await db.delete(keyid, role);
} else {
log(`Unknown transaction type: ${type}`);
continue;
}
if (result.status !== 200) {
log(`Transaction ${tx.id} failed with status ${result.status}: ${result.msg}`);
} else {
log(`Transaction ${tx.id} applied successfully`);
}
} catch (error) {
log(`Failed to apply transaction ${tx.id}:`, error.message);
}
}
}
/**
* Get pending transactions (for Consensus module)
*/
static getPendingTransactions() {
return pendingTransactions;
}
/**
* Get blockchain (for Consensus module)
*/
static getBlockchain() {
return blockchain;
}
/**
* Add block to blockchain (called by Consensus after approval)
*/
static addBlock(block) {
blockchain.push(block);
pendingTransactions = [];
log(`Block #${block.index} added to blockchain`);
}
/**
* Starts the blockchain process intervals.
*/
static start() {
log("Blockchain process started.");
// Start consensus if enabled
if (process.env.CONSENSUS_ENABLED === 'true') {
const Consensus = require('./Consensus.js');
Consensus.start().catch(err => log("Consensus start error:", err.message));
log("Consensus mechanism enabled");
} else {
// Fallback: old simple block creation
log("Consensus disabled. Using simple block creation every 10 minutes.");
setInterval(this.createBlock.bind(this), 10 * 60 * 1000);
}
}
static async syncFromNetwork(nationId, seedDomain) {
log(`Starting blockchain sync for nation: ${nationId}${seedDomain ? ' (seed ' + seedDomain + ')' : ''}`);
try {
const townsDb = new Odmdb('towns', process.env.TOWN || 'apxtri');
const nationsDb = new Odmdb('nations', process.env.TOWN || 'apxtri');
// 1) candidate seeds: explicit seedDomain, nation bootstrap, local towns of the nation
const seeds = [];
if (seedDomain) seeds.push(seedDomain);
try {
const nationResult = await nationsDb.read(nationId, { xprofils: ['admin'] });
if (nationResult.status !== 200) {
return { status: 404, ref: "Blockchain", msg: "nation_not_found_for_sync", data: { nationId } };
}
if (nationResult.data) {
const bs = nationResult.data.bootstrap || nationResult.data.nationlst || [];
(Array.isArray(bs) ? bs : []).forEach(s => {
if (s && s.dns && s.dns[0]) seeds.push(s.dns[0]);
});
}
} catch {}
const allTownsPath = path.join(TRIBE_PATH, 'objects', 'towns', 'idx', 'lst_townId.json');
let townsList = [];
try { townsList = await fs.readJson(allTownsPath); } catch { townsList = []; }
if (Array.isArray(townsList)) {
for (const townId of townsList) {
if (townId === process.env.TOWN) continue;
try {
const t = await townsDb.read(townId, { xprofils: ['admin'] });
if (t.status === 200 && t.data && t.data.dns && t.data.dns[0]
&& (t.data.nationId === nationId || String(townId).includes(nationId))) {
seeds.push(t.data.dns[0]);
}
} catch {}
}
}
const uniqueSeeds = [...new Set(seeds)];
// 2) probe each seed (https then http) and keep the longest chain
let bestSeed = null;
let bestLength = -1;
for (const dns of uniqueSeeds) {
for (const proto of ['https', 'http']) {
const url = `${proto}://${dns}/apxtri/blockchain/info`;
try {
const r = await axios.get(url, { timeout: 8000, validateStatus: () => true });
if (r.status === 200 && r.data && r.data.data) {
const length = r.data.data.length || 0;
log(`Probed ${url}: length=${length}`);
if (length > bestLength) { bestLength = length; bestSeed = dns; }
break; // seed answered, no need to retry the other protocol
}
} catch {}
}
}
if (!bestSeed) {
log("No reachable towns found, initializing empty blockchain");
return { status: 200, ref: "Blockchain", msg: "no_reachable_towns", data: { blocks: 0 } };
}
// 3) download the chain from the reachable seed (trailing slash required)
let blockchainResponse;
try {
blockchainResponse = await axios.get(`https://${bestSeed}/apxtri/blockchain/`, { timeout: 30000, validateStatus: () => true });
} catch (e) {
const httpUrl = `http://${bestSeed}/apxtri/blockchain/`;
log(`HTTPS download failed (${e.message}), trying ${httpUrl}`);
blockchainResponse = await axios.get(httpUrl, { timeout: 30000, validateStatus: () => true });
}
if (!blockchainResponse || blockchainResponse.status !== 200 || !blockchainResponse.data || !blockchainResponse.data.data || !blockchainResponse.data.data.blockchain) {
return { status: 500, ref: "Blockchain", msg: "failed_to_download_blockchain" };
}
const downloadedBlockchain = blockchainResponse.data.data.blockchain;
log(`Downloaded ${downloadedBlockchain.length} blocks from ${bestSeed}`);
const syncResult = await this.smartMergeBlockchain(downloadedBlockchain);
return {
status: 200,
ref: "Blockchain",
msg: "blockchain_sync_complete",
data: {
blocks: downloadedBlockchain.length,
leader: bestSeed,
applied: syncResult.applied,
skipped: syncResult.skipped
}
};
} catch (error) {
console.error('[Blockchain] Sync error:', error);
return { status: 500, ref: "Blockchain", msg: "blockchain_sync_failed", error: error.message };
}
}
static async smartMergeBlockchain(downloadedChain) {
log(`Starting smart merge of ${downloadedChain.length} blocks`);
let applied = 0;
let skipped = 0;
for (const block of downloadedChain) {
if (blockchain.some(b => b.hash === block.hash)) {
skipped++;
continue;
}
if (await this._verifySignature(block)) {
blockchain.push(block);
await this.updateLocalData(block);
applied++;
log(`Applied block #${block.index} from validator ${block.validator}`);
} else {
log(`Skipped invalid block #${block.index}`);
skipped++;
}
}
blockchain.sort((a, b) => a.index - b.index);
log(`Smart merge complete: ${applied} applied, ${skipped} skipped`);
return { applied, skipped };
}
static async createGenesisBlock(mayorAlias) {
log(`Creating genesis block for nation by mayor: ${mayorAlias}`);
const transactions = [];
try {
const pagansDb = new Odmdb('pagans', process.env.TOWN || 'apxtri');
const nationsDb = new Odmdb('nations', process.env.TOWN || 'apxtri');
const townsDb = new Odmdb('towns', process.env.TOWN || 'apxtri');
const APX3 = require('./APX3.js');
const pagansPath = path.join(TRIBE_PATH, 'objects', 'pagans', 'itm');
const nationsPath = path.join(TRIBE_PATH, 'objects', 'nations', 'itm');
const townsPath = path.join(TRIBE_PATH, 'objects', 'towns', 'itm');
let paganCount = 0;
if (await fs.pathExists(pagansPath)) {
const paganFiles = await fs.readdir(pagansPath);
for (const file of paganFiles) {
if (!file.endsWith('.json')) continue;
const pagan = await fs.readJson(path.join(pagansPath, file));
transactions.push({
id: crypto.randomUUID(),
type: 'create',
model: 'pagans',
payload: pagan
});
paganCount++;
}
log(`Added ${paganCount} pagans to genesis block`);
}
let nationCount = 0;
if (await fs.pathExists(nationsPath)) {
const nationFiles = await fs.readdir(nationsPath);
for (const file of nationFiles) {
if (!file.endsWith('.json')) continue;
const nation = await fs.readJson(path.join(nationsPath, file));
transactions.push({
id: crypto.randomUUID(),
type: 'create',
model: 'nations',
payload: nation
});
nationCount++;
}
log(`Added ${nationCount} nations to genesis block`);
}
let townCount = 0;
if (await fs.pathExists(townsPath)) {
const townFiles = await fs.readdir(townsPath);
for (const file of townFiles) {
if (!file.endsWith('.json')) continue;
const town = await fs.readJson(path.join(townsPath, file));
transactions.push({
id: crypto.randomUUID(),
type: 'create',
model: 'towns',
payload: town
});
townCount++;
}
log(`Added ${townCount} towns to genesis block`);
}
const config = await Nations.getConfig();
const createTownFee = config.createTownFee;
const createPaganFee = config.createPaganFee;
const createPersonFee = config.createPersonFee;
const totalAPX3 = (townCount * createTownFee) +
(paganCount * createPaganFee) +
(100 * createPersonFee);
log(`Genesis APX3 allocation: ${totalAPX3} APX3 to ${mayorAlias}`);
log(` - ${townCount} towns x ${createTownFee} = ${townCount * createTownFee}`);
log(` - ${paganCount} pagans x ${createPaganFee} = ${paganCount * createPaganFee}`);
log(` - 100 persons x ${createPersonFee} = ${100 * createPersonFee}`);
const genesisMintTx = await APX3.createGenesisMint(
mayorAlias,
totalAPX3,
`Genesis allocation for ${paganCount} pagans, ${townCount} towns, 100 persons`
);
transactions.push({
id: genesisMintTx.txid,
type: 'genesis_mint',
model: 'apx3',
payload: genesisMintTx
});
const genesisBlock = {
index: 0,
timestamp: Date.now(),
transactions,
previousHash: "0".repeat(64),
validator: 'genesis',
hash: this._calculateHash({
index: 0,
timestamp: Date.now(),
transactions,
previousHash: "0".repeat(64),
validator: 'genesis'
}),
signature: null
};
blockchain.push(genesisBlock);
pendingTransactions = [];
log(`Genesis block created with ${transactions.length} transactions`);
log(`Genesis block hash: ${genesisBlock.hash}`);
return {
status: 200,
ref: "Blockchain",
msg: "genesis_block_created",
data: {
blockHash: genesisBlock.hash,
transactions: transactions.length,
pagans: paganCount,
nations: nationCount,
towns: townCount,
apx3Allocated: totalAPX3,
mayor: mayorAlias
}
};
} catch (error) {
console.error('[Blockchain] Genesis block creation failed:', error);
return {
status: 500,
ref: "Blockchain",
msg: "genesis_block_creation_failed",
error: error.message
};
}
}
}
module.exports = Blockchain;