520 lines
20 KiB
JavaScript
Executable File
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; |