miner mines continuously,

moved to an account model,
transactions working,
querying working,
more checking of valid data
This commit is contained in:
Josip Milovac 2023-01-10 17:11:56 +11:00
parent 59bb42be11
commit 9847b2056b
12 changed files with 663 additions and 365 deletions

View file

@ -1,12 +1,20 @@
const ChainUtil = require('../chain-util');
const { DIFFICULTY, MINE_RATE } = require('../config');
function concatIfNotUndefined(concatTo, concatting) {
if (typeof concatting !== "undefined") {
concatTo += `${concatting}`;
}
}
class Block {
constructor(timestamp, lastHash, hash, data, nonce, difficulty) {
constructor(timestamp, lastHash, hash, reward, transactions, metadatas, nonce, difficulty) {
this.timestamp = timestamp;
this.lastHash = lastHash;
this.hash = hash;
this.data = data;
this.reward = reward;
this.transactions = transactions;
this.metadatas = metadatas;
this.nonce = nonce;
if (difficulty === undefined) {
this.difficulty = DIFFICULTY;
@ -15,54 +23,90 @@ class Block {
}
}
static getTransactions(block) {
if (typeof block.transactions !== "undefined" && block.transactions !== null) {
return block.transactions;
} else {
return [];
}
}
static getMetadatas(block) {
if (typeof block.metadatas !== "undefined" && block.metadatas !== null) {
return block.metadatas;
} else {
return [];
}
}
toString() {
return `Block -
Timestamp : ${this.timestamp}
Last Hash : ${this.lastHash.substring(0, 10)}
Hash : ${this.hash.substring(0, 10)}
Nonce : ${this.nonce}
Difficulty: ${this.difficulty}
Data : ${this.data}`;
Timestamp : ${this.timestamp}
Last Hash : ${this.lastHash.substring(0, 10)}
Hash : ${this.hash.substring(0, 10)}
Nonce : ${this.nonce}
Difficulty : ${this.difficulty}
Reward : ${this.reward}
Transactions : ${this.transactions}
Metadatas : ${this.metadatas}`;
}
static genesis() {
return new this('Genesis time', '-----', 'f1r57-h45h', [], 0, DIFFICULTY);
return new this('Genesis time', '-----', 'f1r57-h45h', null, null, null, 0, DIFFICULTY);
}
//returns false if hash doesn't match
static checkHash(hash, timestamp, lastHash, data, nonce, difficulty) {
const computedHash = Block.hash(timestamp, lastHash, data, nonce, difficulty);
static hash(timestamp, lastHash, reward, transactions, metadatas, nonce, difficulty) {
//backwards compatible hashing:
//if we add a new type of thing to the chain, the hash of previous blocks won't change as if will be undefined
let hashing = `${timestamp}${lastHash}${nonce}${difficulty}`;
concatIfNotUndefined(hashing, reward);
concatIfNotUndefined(hashing, transactions);
concatIfNotUndefined(hashing, metadatas);
if (computedHash !== hash) {
return ChainUtil.hash(hashing).toString();
}
static blockHash(block) {
return Block.hash(
block.timestamp,
block.lastHash,
block.reward,
block.transactions,
block.metadatas,
block.nonce,
block.difficulty);
}
//returns false if block's hash doesn't match internals
static checkHash(block) {
const computedHash = Block.hash(
block.timestamp,
block.lastHash,
block.reward,
block.transactions,
block.metadatas,
block.nonce,
block.difficulty);
if (computedHash !== block.hash) {
return false;
}
if (hash.substring(0, difficulty) !== '0'.repeat(difficulty)) {
if (block.hash.substring(0, block.difficulty) !== '0'.repeat(block.difficulty)) {
return false;
}
return true;
}
static hash(timestamp, lastHash, data, nonce, difficulty) {
return ChainUtil.hash(`${timestamp}${lastHash}${data}${nonce}${difficulty}`).toString();
}
static blockHash(block) {
const { timestamp, lastHash, data, nonce, difficulty } = block;
return Block.hash(timestamp, lastHash, data, nonce, difficulty);
}
//returns false if block's hash doesn't match internals
static checkBlock(block) {
return Block.checkHash(block.hash, block.timestamp, block.lastHash, block.data, block.nonce, block.difficulty);
}
static adjustDifficulty(lastBlock, currentTime) {
let { difficulty } = lastBlock;
difficulty = lastBlock.timestamp + MINE_RATE > currentTime ?
difficulty + 1 : difficulty - 1;
return Math.max(0, difficulty);
let prevDifficulty = lastBlock.difficulty;
if (lastBlock.timestamp + MINE_RATE > currentTime) {
return prevDifficulty + 1;
} else {
return Math.max(0, prevDifficulty - 1);
}
}
}

View file

@ -1,81 +1,276 @@
const Block = require('./block');
const N3 = require('n3');
const DataFactory = require('n3').DataFactory;
const Transaction = require('../wallet/transaction');
const { MINING_REWARD } = require('../config');
function getBalanceCopyGeneric(publicKey, maps) {
for (const map of maps) {
if (map.hasOwnProperty(publicKey)) {
const found = map[publicKey];
return {
balance: found.balance,
counter: found.counter
};
}
}
return {
balance: 0,
counter: 0
};
}
function verifyBlock(prevBalances, prevBlock, verifyingBlock) {
if (verifyingBlock.lastHash !== prevBlock.hash) {
return {
result: false,
reason: "last hash didn't match our last hash"
};
}
//how to check if new block's timestamp is believable
if (verifyingBlock.difficulty !== Block.adjustDifficulty(prevBlock, verifyingBlock.timestamp)) {
return {
result: false,
reason: "difficulty is incorrect"
};
}
if (!Block.checkHash(verifyingBlock)) {
return {
result: false,
reason: "hash is invalid failed"
};
}
const changedBalances = {};
const rewardBalanceCopy = getBalanceCopyGeneric(verifyingBlock.reward, [prevBalances]);
changedBalances[verifyingBlock.reward] = {
balance: rewardBalanceCopy.balance + MINING_REWARD,
counter: rewardBalanceCopy.counter
};
for (const transaction of Block.getTransactions(verifyingBlock)) {
if (!Transaction.verify(transaction)) {
return {
result: false,
reason: "couldn't verify a transaction" };
}
const inputBalance = getBalanceCopyGeneric(transaction.input, [changedBalances, prevBalances]);
if (transaction.counter <= inputBalance.counter) {
return {
result: false,
reason: "transaction has invalid counter"
};
}
inputBalance.counter = transaction.counter;
for (const output of transaction.outputs) {
const outputBalance = getBalanceCopyGeneric(output.publicKey, [changedBalances, prevBalances]);
if (output.amount > inputBalance.balance) {
return {
result: false,
reason: "transaction spending more than they have"
};
}
inputBalance.balance -= output.amount;
outputBalance.balance += output.amount;
changedBalances[output.publicKey] = outputBalance;
}
changedBalances[transaction.input] = inputBalance;
}
return {
result: true,
changedBalances: changedBalances
};
}
function verifyChain(chain) {
if (chain.length === 0) {
return {
result: false,
reason: "zero length"
};
}
if (JSON.stringify(chain[0]) !== JSON.stringify(Block.genesis())) {
return {
result: false,
reason: "initial block isn't genesis"
};
}
const balances = {};
for (let i = 1; i < chain.length; i++) {
const block = chain[i];
const lastBlock = chain[i - 1];
const verifyResult = verifyBlock(balances, lastBlock, block);
if (verifyResult.result === false) {
return {
result: false,
reason: `Chain is invalid on block ${i}: ${verifyResult.reason}`
};
}
for (const publicKey in verifyResult.changedBalances) {
balances[publicKey] = verifyResult.changedBalances[publicKey];
}
}
return {
result: true,
balances: balances
};
}
//returns the first index where the two chains differ
function findChainDifference(oldChain, newChain) {
for (let i = 1; i < oldChain.length; ++i) {
if (oldChain[i].hash !== newChain[i].hash) {
return i;
}
}
return 1;
}
function addBlockMetadata(blockchain, block) {
const metadatas = Block.getMetadatas(block);
for (const key in metadatas) {
const metadata = metadatas[key];
if (!("SSNmetadata" in metadata)) {
//assert?
return;
}
var ssn = metadata.SSNmetadata;
const parser = new N3.Parser();
parser.parse(
ssn,
(error, quadN, prefixes) => {
if (quadN) {
blockchain.store.addQuad(DataFactory.quad(
DataFactory.namedNode(quadN.subject.id),
DataFactory.namedNode(quadN.predicate.id),
DataFactory.namedNode(quadN.object.id),
DataFactory.namedNode(block.hash)));
}
});
}
}
class Blockchain {
constructor() {
this.chain = [Block.genesis()];
this.balances = {};
this.store = new N3.Store();
}
getBalanceCopy(publicKey) {
return getBalanceCopyGeneric(publicKey, [this.balances]);
}
lastBlock() {
return this.chain[this.chain.length - 1];
}
serialize() {
return JSON.stringify(this.chain);
}
static deserialize(serialized) {
const returning = new Blockchain();
const replaceResult = returning.replaceChain(JSON.parse(serialized));
if(!replaceResult.result) {
//chain wasn't valid
return null;
} else {
return returning;
}
}
//adds an existing block to the blockchain, returns false if the block can't be added, true if it was added
addBlock(newBlock) {
if (newBlock.lastHash !== this.chain[this.chain.length - 1].hash) {
console.log("Tried to add invalid block, last hash didn't match our last hash");
return false;
}
//how to check if new block's timestamp is believable
if (newBlock.difficulty !== Block.adjustDifficulty(this.chain[this.chain.length - 1], newBlock.timestamp)) {
console.log("Tried to add invalid block, difficulty is incorrect");
return false;
}
if (!Block.checkBlock(newBlock)) {
console.log("Tried to add invalid block, block's hash doesn't match its contents");
return false;
}
const verifyResult = verifyBlock(this.balances, this.lastBlock(), newBlock);
if (!verifyResult.result) {
console.log(`Couldn't add block: ${verifyResult.reason}`);
return false;
}
//all seems to be good, persist
this.chain.push(newBlock);
console.log("Added new block: ");
for (const publicKey in verifyResult.changedBalances) {
this.balances[publicKey] = verifyResult.changedBalances[publicKey];
}
addBlockMetadata(this, newBlock);
//console.log("Added new block");
//console.log(newBlock);
return true;
}
isValidChain(chain) {
if (chain.length === 0) {
return false;
}
if (JSON.stringify(chain[0]) !== JSON.stringify(Block.genesis())) {
return false;
}
static isValidChain(chain) {
const res = verifyChain(chain);
for (let i=1; i<chain.length; i++) {
const block = chain[i];
const lastBlock = chain[i-1];
if (block.lastHash !== lastBlock.hash ||
block.hash !== Block.blockHash(block)) {
return false;
}
if (!Block.checkBlock(block)) {
return false;
}
}
return true;
return res.result;
}
//return null on fail, returns the index of where they differ
//return false on fail, true on success
//TODO: faster verification of the new chain by only verifying from divergence, would require saving some historical balance state
replaceChain(newChain) {
if (newChain.length <= this.chain.length) {
console.log('Received chain is not longer than the current chain.');
return false;
} else if (!this.isValidChain(newChain)) {
console.log('The received chain is not valid.');
return false;
return {
result: false,
reason: "Received chain is not longer than the current chain."
};
}
const verifyResult = verifyChain(newChain);
if (!verifyResult.result) {
return {
result: false,
reason: `The received chain is not valid: ${verifyResult.reason}`
};
}
console.log('Replacing blockchain with the new chain.');
//Replacing blockchain with the new chain
const oldChain = this.chain;
this.chain = newChain;
//find where they differ
for (let i = 1; i < oldChain.length; ++i) {
if (oldChain[i].hash !== newChain[i].hash) {
return i;
}
const chainDifference = findChainDifference(oldChain, newChain);
console.log(`chain difference was ${chainDifference}`);
//fix metadata
for (let i = oldChain.length - 1; i >= chainDifference; i--) {
this.store.deleteGraph(oldChain[i].hash);
}
//if they didn't differ in the length of the old chain, must be one after
return oldChain.length;
for (let i = chainDifference; i < newChain.length; ++i) {
addBlockMetadata(this, newChain[i]);
}
//fix balance
this.balances = verifyResult.balances;
return {
result: true,
chainDifference: chainDifference,
oldChain: oldChain
};
}
}