diff --git a/README.md b/README.md index 560b396045..eecb91ce91 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Remember that each data has its own trade-offs. And you need to pay attention mo * `A` [Disjoint Set](src/data-structures/disjoint-set) - a union–find data structure or merge–find set * `A` [Bloom Filter](src/data-structures/bloom-filter) * `A` [LRU Cache](src/data-structures/lru-cache/) - Least Recently Used (LRU) cache +* `A` [Blockchain](src/data-structures/blockchain/) - Blockchain ## Algorithms diff --git a/src/data-structures/blockchain/Blockchain.js b/src/data-structures/blockchain/Blockchain.js new file mode 100644 index 0000000000..c1d767dd98 --- /dev/null +++ b/src/data-structures/blockchain/Blockchain.js @@ -0,0 +1,194 @@ +import Block from './BlockchainBlock'; +import BlockchainTransaction from './BlockchainTransaction'; + +export default class Blockchain { + /** + * @param {number} [difficulty=2] The difficulty level for mining new blocks + * Higher values require more computational work + */ + constructor(difficulty = 2) { + this.difficulty = difficulty; + /** + * @type {Array} + */ + this.chain = []; + /** + * @type {Array} + */ + this.pendingTransactions = []; + /** + * @type {number} + */ + this.minerReward = 10; + /** + * @type {Map} + */ + this.balances = new Map(); + /** + * @type {Map>} + */ + this.transactionsByAddress = new Map(); + this.createGenesisBlock(); + } + + /** + * Creates the first block in the chain. + * @private + */ + createGenesisBlock() { + const genesisBlock = new Block({ + index: 0, + data: 'Genesis Block', + previousHash: '0', + }); + genesisBlock.mineBlock(this.difficulty); + this.chain.push(genesisBlock); + } + + /** + * @returns {Block} The last block in the chain + */ + getLatestBlock() { + return this.chain[this.chain.length - 1]; + } + + /** + * @param {string} address The address whose balance should be adjusted + * @param {number} amount The amount to add to the current balance + */ + updateBalance(address, amount) { + this.balances.set(address, (this.balances.get(address) || 0) + amount); + } + + /** + * @param {number} blockIndex The index of the block that stores the transaction + * @param {BlockchainTransaction} transaction The transaction to index + */ + indexTransaction(blockIndex, transaction) { + const addresses = new Set([transaction.from, transaction.to]); + for (const address of addresses) { + const addressTransactions = this.transactionsByAddress.get(address) || []; + addressTransactions.push({ + block: blockIndex, + transaction, + }); + this.transactionsByAddress.set(address, addressTransactions); + } + } + + /** + * @param {BlockchainTransaction} transaction The transaction object to add + * @returns {boolean} True if the transaction was added successfully + */ + addTransaction(transaction) { + // Basic validation: in production, use more robust checks + if (!transaction.from || !transaction.to || !transaction.amount) { + return false; + } + + if (transaction.amount <= 0) { + // Invalid transaction: amount must be positive + return false; + } + + this.pendingTransactions.push(transaction); + this.updateBalance(transaction.from, -transaction.amount); + this.updateBalance(transaction.to, transaction.amount); + + return true; + } + + /** + * Mines pending transactions into a new block and rewards the miner. + * This method creates a new block, mines it according to the difficulty level, + * and adds it to the blockchain. + * + * @param {string} minerAddress - The address of the miner who will receive the reward + * @returns {Block|null} The newly mined block, or null if no pending transactions exist + */ + minePendingTransactions(minerAddress) { + // Add miner reward transaction + const transaction = new BlockchainTransaction({ + from: 'System', + to: minerAddress, + amount: this.minerReward, + description: 'Mining Reward', + }); + this.pendingTransactions.push(transaction); + this.updateBalance(transaction.from, -transaction.amount); + this.updateBalance(transaction.to, transaction.amount); + + // Create new block with pending transactions + const newBlock = new Block({ + index: this.chain.length, + data: this.pendingTransactions, + previousHash: this.getLatestBlock().hash, + }); + + // Mine the block + newBlock.mineBlock(this.difficulty); + + // Add to chain + this.chain.push(newBlock); + + for (const transaction of newBlock.data) { + this.indexTransaction(newBlock.index, transaction); + } + + // Clear pending transactions + this.pendingTransactions = []; + + return newBlock; + } + + /** + * Validates the entire blockchain by checking: + * 1. Each block's hash is correctly calculated + * 2. Each block's previous hash matches the previous block's hash + * 3. No blocks have been tampered with + * + * @returns {boolean} True if the blockchain is valid, false otherwise + */ + isChainValid() { + if (this.chain.length <= 1) { + // An empty chain or a chain with only the genesis block is considered valid + return true; + } + + // Check each block starting from the second block (index 1) + for (let i = 1; i < this.chain.length; i++) { + const currentBlock = this.chain[i]; + const previousBlock = this.chain[i - 1]; + + // Verify current block's hash is correct + if (currentBlock.hash !== currentBlock.calculateHash()) { + // The block has been tampered with + return false; + } + + // Verify link to previous block + if (currentBlock.previousHash !== previousBlock.hash) { + // The link to the previous block is invalid + return false; + } + } + + return true; + } + + /** + * @param {string} address The address to check balance for + * @returns {number} The total balance of the address + */ + getBalance(address) { + return this.balances.get(address) || 0; + } + + /** + * @param {string} address The address to find transactions for + * @returns {Array} An array of transaction objects involving the address + */ + getTransactionsForAddress(address) { + return (this.transactionsByAddress.get(address) || []).slice(); + } +} diff --git a/src/data-structures/blockchain/BlockchainBlock.js b/src/data-structures/blockchain/BlockchainBlock.js new file mode 100644 index 0000000000..989bf4db58 --- /dev/null +++ b/src/data-structures/blockchain/BlockchainBlock.js @@ -0,0 +1,53 @@ +import crypto from 'crypto'; + +export default class Block { + /** + * @param {Object} blockParams - The block parameters + * @param {number} blockParams.index - The position of the block in the blockchain + * @param {string} [blockParams.timestamp] - The creation time of the block in ISO format + * @param {Array|String} blockParams.data - The transaction data stored in this block + * @param {string} blockParams.previousHash - The hash of the previous block in the chain + * @param {number} [blockParams.nonce=0] - The number used once for proof of work calculations + */ + constructor({ + index, + timestamp = new Date().toISOString(), + data, + previousHash, + nonce = 0, + }) { + this.index = index; + this.timestamp = timestamp; + this.data = data; + this.previousHash = previousHash; + this.nonce = nonce; + this.hash = this.calculateHash(); + } + + /** + * @private + * @returns {string} The hexadecimal representation of the block's hash + */ + calculateHash() { + const blockData = JSON.stringify({ + index: this.index, + timestamp: this.timestamp, + data: this.data, + previousHash: this.previousHash, + nonce: this.nonce, + }); + + return crypto.createHash('sha256').update(blockData).digest('hex'); + } + + /** + * @param {number} difficulty - The number of leading zeros required in the hash + */ + mineBlock(difficulty) { + const target = '0'.repeat(difficulty); + while (this.hash.substring(0, difficulty) !== target) { + this.nonce += 1; + this.hash = this.calculateHash(); + } + } +} diff --git a/src/data-structures/blockchain/BlockchainTransaction.js b/src/data-structures/blockchain/BlockchainTransaction.js new file mode 100644 index 0000000000..ae8cbedfec --- /dev/null +++ b/src/data-structures/blockchain/BlockchainTransaction.js @@ -0,0 +1,20 @@ +export default class BlockchainTransaction { + /** + * @param {Object} transactionParams - The transaction parameters + * @param {string} transactionParams.from - The sender's address + * @param {string} transactionParams.to - The recipient's address + * @param {number} transactionParams.amount - The transaction amount + * @param {string} [transactionParams.description=''] - Optional description of the transaction + */ + constructor({ + from, + to, + amount, + description = '', + }) { + this.from = from; + this.to = to; + this.amount = amount; + this.description = description; + } +} diff --git a/src/data-structures/blockchain/README.md b/src/data-structures/blockchain/README.md new file mode 100644 index 0000000000..000b3a900f --- /dev/null +++ b/src/data-structures/blockchain/README.md @@ -0,0 +1,209 @@ +# Blockchain + +## What is Blockchain? + +A blockchain is a distributed ledger technology that maintains a chain of blocks, where each block contains cryptographically secured data. Each block is linked to the previous block through a hash reference, creating an immutable chain. Any attempt to modify a past block would change its hash, breaking the chain and making the tampering detectable. + +Key characteristics: + +- **Immutability**: Once data is recorded, it cannot be altered without detection. +- **Transparency**: All transactions are visible to participants. +- **Decentralization**: No single point of control. +- **Security**: Uses cryptographic hashing to secure data. + +![Blockchain](./images/blockchain.jpg) + +## Why Use Blockchain? + +1. **Data Integrity**: Ensures that records cannot be secretly modified. +2. **Auditability**: Complete history of all transactions is preserved. +3. **Trust**: Eliminates the need for a trusted intermediary. +4. **Security**: Cryptographic hashing makes the data tamper-evident. +5. **Transparency**: All participants have access to the same ledger. + +## How to Use It + +### Basic Setup + +Import the required classes: + +```javascript +import Blockchain from './Blockchain'; +import BlockchainTransaction from './BlockchainTransaction'; +``` + +### Creating a Blockchain + +Create a new blockchain instance with a specified difficulty level, e.g. 3: + +```javascript +const blockchain = new Blockchain(3); +``` + +**Constructor Parameter**: + +- `difficulty` (number, default: 2): The number of leading zeros required in a block's hash during mining. Higher number means higher difficulty. + +### Adding Transactions + +Create transactions and add them to the pending transactions pool: + +```javascript +const transaction1 = new BlockchainTransaction({ + from: 'Alice', + to: 'Bob', + amount: 50, + description: 'Payment for services', +}); +blockchain.addTransaction(transaction1); + +const transaction2 = new BlockchainTransaction({ + from: 'Bob', + to: 'Charlie', + amount: 25, +}); +blockchain.addTransaction(transaction2); +``` + +**Transaction Class Properties**: + +- `from`: The sender's address +- `to`: The recipient's address +- `amount`: The transaction amount (must be positive) +- `description`: Optional description of the transaction + +### Mining Blocks + +Mine pending transactions into a new block: + +```javascript +const minedBlock = blockchain.minePendingTransactions('Miner1'); +``` + +### Block Structure + +Each block contains: + +```javascript +index; // Position in blockchain +timestamp; // Creation time in ISO format +data; // Array of transactions in this block +previousHash; // Hash of the previous block +hash; // Current block's hash +nonce; // Number used in proof-of-work calculation +``` + +### Validating the Blockchain + +Verify the integrity of the entire blockchain: + +```javascript +if (blockchain.isChainValid()) { + // Blockchain is valid +} else { + // Blockchain has been tampered with! +} +``` + +### Checking Balances + +Get the balance of an address by analyzing all transactions: + +```javascript +const aliceBalance = blockchain.getBalance('Alice'); +console.log(aliceBalance); // 10 +``` + +### Retrieving Transactions + +Find all transactions involving a specific address: + +```javascript +const aliceTransactions = blockchain.getTransactionsForAddress('Alice'); +console.log(aliceTransactions) +// [ +// { block: 1, transaction: {...} }, +// { block: 2, transaction: {...} } +// ] +``` + +### Getting Latest Block + +Access the most recent block in the chain: + +```javascript +const latestBlock = blockchain.getLatestBlock(); +``` + +## Complete Example + +```javascript +import Blockchain from './Blockchain.js'; +import BlockchainTransaction from './BlockchainTransaction.js'; + +const blockchain = new Blockchain(2); + +blockchain.addTransaction(new BlockchainTransaction({ + from: 'Alice', + to: 'Bob', + amount: 30, +})); +blockchain.addTransaction(new BlockchainTransaction({ + from: 'Bob', + to: 'Charlie', + amount: 15, +})); + +blockchain.minePendingTransactions('Miner1'); + +blockchain.addTransaction(new BlockchainTransaction({ + from: 'Charlie', + to: 'Alice', + amount: 10, +})); + +blockchain.minePendingTransactions('Miner2'); +``` + +## Implementation Details + +### Mining and Difficulty + +The `mineBlock(difficulty)` method in BlockchainBlock implements proof-of-work: + +- It increments the `nonce` value until the resulting `hash` has the required number of leading zeros. +- Higher difficulty values require exponentially more computational work. +- This mechanism secures the blockchain by making it computationally expensive to tamper with blocks. + +### Chain Validation + +The `isChainValid()` method ensures: + +- Each block's hash matches its calculated hash (prevents tampering). +- Each block's `previousHash` matches the previous block's `hash` (maintains chain integrity). +- The entire chain is unbroken. + +### Pending Transactions + +Transactions are held in `pendingTransactions` until they are included in a mined block. This allows: + +- Multiple transactions to be batched together. +- Efficient block creation. +- Flexibility in mining timing. + +## Security Considerations + +- **Difficulty**: Increase `difficulty` for higher security (but slower mining). +- **Validation**: Always call `isChainValid()` before trusting the blockchain. +- **Address Verification**: This implementation doesn't include cryptographic signatures. In production, use digital signatures to verify transaction authenticity. +- **Double Spending**: Implement balance checks before accepting transactions in production systems. + +## References + +This documentation draws from fundamental blockchain concepts and implementation details. Key sources include: + +- **Bitcoin Whitepaper**: Nakamoto, S. (2008). "Bitcoin: A Peer-to-Peer Electronic Cash System." Available at: https://bitcoin.org/bitcoin.pdf +- **Mastering Bitcoin**: Antonopoulos, A. M. (2017). "Mastering Bitcoin: Programming the Open Blockchain." O'Reilly Media. +- **Blockchain Technology Overview**: Swan, M. (2015). "Blockchain: Blueprint for a New Economy." O'Reilly Media. +- **Ethereum Documentation**: https://ethereum.org/en/developers/docs/ +- **Wikipedia - Blockchain**: https://en.wikipedia.org/wiki/Blockchain diff --git a/src/data-structures/blockchain/__test__/Blockchain.test.js b/src/data-structures/blockchain/__test__/Blockchain.test.js new file mode 100644 index 0000000000..bab20274dd --- /dev/null +++ b/src/data-structures/blockchain/__test__/Blockchain.test.js @@ -0,0 +1,108 @@ +import Blockchain from '../Blockchain'; +import BlockchainTransaction from '../BlockchainTransaction'; + +describe('Blockchain', () => { + it('should initialize with a mined genesis block', () => { + const blockchain = new Blockchain(1); + const [genesisBlock] = blockchain.chain; + expect(blockchain.difficulty).toBe(1); + expect(blockchain.chain).toHaveLength(1); + expect(blockchain.balances).toEqual(new Map()); + expect(blockchain.transactionsByAddress).toEqual(new Map()); + expect(genesisBlock.index).toBe(0); + expect(genesisBlock.data).toBe('Genesis Block'); + expect(genesisBlock.previousHash).toBe('0'); + expect(genesisBlock.hash.startsWith('0')).toBe(true); + expect(blockchain.getLatestBlock()).toBe(genesisBlock); + }); + + it('should validate and reject pending transactions based on required fields and amount', () => { + const blockchain = new Blockchain(0); + const validTransaction = new BlockchainTransaction({ + from: 'alice', + to: 'bob', + amount: 15, + description: 'Invoice', + }); + const invalidTransaction1 = new BlockchainTransaction({ + from: '', + to: 'bob', + amount: 15, + }); + const invalidTransaction2 = new BlockchainTransaction({ + from: 'alice', + to: '', + amount: 15, + }); + const invalidTransaction3 = new BlockchainTransaction({ + from: 'alice', + to: 'bob', + amount: 0, + }); + const invalidTransaction4 = new BlockchainTransaction({ + from: 'alice', + to: 'bob', + amount: -5, + }); + expect(blockchain.addTransaction(validTransaction)).toBe(true); + expect(blockchain.addTransaction(invalidTransaction1)).toBe(false); + expect(blockchain.addTransaction(invalidTransaction2)).toBe(false); + expect(blockchain.addTransaction(invalidTransaction3)).toBe(false); + expect(blockchain.addTransaction(invalidTransaction4)).toBe(false); + expect(blockchain.pendingTransactions).toEqual([validTransaction]); + expect(blockchain.getBalance('alice')).toBe(-15); + expect(blockchain.getBalance('bob')).toBe(15); + expect(blockchain.getTransactionsForAddress('alice')).toEqual([]); + blockchain.minePendingTransactions('miner-address'); + expect(blockchain.getTransactionsForAddress('alice')).toEqual([ + { block: 1, transaction: validTransaction }, + ]); + }); + + it('should mine pending transactions, reward miner, and update balances and transaction history', () => { + const blockchain = new Blockchain(1); + const firstTransaction = new BlockchainTransaction({ + from: 'alice', + to: 'bob', + amount: 25, + description: 'Payment', + }); + const secondTransaction = new BlockchainTransaction({ + from: 'bob', + to: 'carol', + amount: 5, + description: 'Refund', + }); + const minerAddress = 'miner-address'; + blockchain.addTransaction(firstTransaction); + blockchain.addTransaction(secondTransaction); + const minedBlock = blockchain.minePendingTransactions(minerAddress); + expect(blockchain.chain).toHaveLength(2); + expect(minedBlock.index).toBe(1); + expect(minedBlock.previousHash).toBe(blockchain.chain[0].hash); + expect(minedBlock.hash.startsWith('0')).toBe(true); + expect(minedBlock.data).toEqual([ + firstTransaction, + secondTransaction, + // miner reward transaction + new BlockchainTransaction({ + from: 'System', + to: minerAddress, + amount: 10, + description: 'Mining Reward', + }), + ]); + expect(blockchain.pendingTransactions).toEqual([]); + expect(blockchain.getBalance('alice')).toBe(-25); + expect(blockchain.getBalance('bob')).toBe(20); + expect(blockchain.getBalance('carol')).toBe(5); + expect(blockchain.getBalance(minerAddress)).toBe(10); + expect(blockchain.getTransactionsForAddress('bob')).toEqual([ + { block: 1, transaction: firstTransaction }, + { block: 1, transaction: secondTransaction }, + ]); + expect(blockchain.isChainValid()).toBe(true); + minedBlock.data[0].amount = 100; + expect(blockchain.isChainValid()).toBe(false); + }); +}); diff --git a/src/data-structures/blockchain/__test__/BlockchainBlock.test.js b/src/data-structures/blockchain/__test__/BlockchainBlock.test.js new file mode 100644 index 0000000000..b19d8abff7 --- /dev/null +++ b/src/data-structures/blockchain/__test__/BlockchainBlock.test.js @@ -0,0 +1,72 @@ +import crypto from 'crypto'; +import Block from '../BlockchainBlock'; +import BlockchainTransaction from '../BlockchainTransaction'; + +describe('BlockchainBlock', () => { + it('should create block with deterministic hash from its contents', () => { + const transactions = [ + new BlockchainTransaction({ + from: 'alice', + to: 'bob', + amount: 25, + description: 'Rent', + }), + ]; + const timestamp = '2026-01-01T12:00:00.000Z'; + const blockParams = { + index: 1, + timestamp, + data: transactions, + previousHash: 'previous-hash', + nonce: 7, + }; + const block = new Block(blockParams); + const expectedHash = crypto + .createHash('sha256') + .update(JSON.stringify(blockParams)) + .digest('hex'); + expect(block.index).toBe(1); + expect(block.timestamp).toBe(timestamp); + expect(block.data).toBe(transactions); + expect(block.previousHash).toBe('previous-hash'); + expect(block.nonce).toBe(7); + expect(block.hash).toBe(expectedHash); + }); + + it('should use current ISO timestamp when timestamp is not provided', () => { + const timestamp = '2026-01-01T12:00:00.000Z'; + jest.useFakeTimers().setSystemTime(new Date(timestamp)); + const block = new Block({ + index: 2, + data: [ + new BlockchainTransaction({ + from: 'alice', + to: 'bob', + amount: 10, + }), + ], + previousHash: 'previous-hash', + }); + expect(block.timestamp).toBe(timestamp); + jest.useRealTimers(); + }); + + it('should mine block until hash satisfies the requested difficulty', () => { + const block = new Block({ + index: 3, + timestamp: '2026-05-31T12:00:00.000Z', + data: [ + new BlockchainTransaction({ + from: 'miner', + to: 'alice', + amount: 5, + }), + ], + previousHash: 'previous-hash', + }); + block.mineBlock(2); + expect(block.hash.startsWith('00')).toBe(true); + expect(block.nonce).toBeGreaterThan(0); + expect(block.hash).toBe(block.calculateHash()); + }); +}); diff --git a/src/data-structures/blockchain/__test__/BlockchainTransaction.test.js b/src/data-structures/blockchain/__test__/BlockchainTransaction.test.js new file mode 100644 index 0000000000..277f128d96 --- /dev/null +++ b/src/data-structures/blockchain/__test__/BlockchainTransaction.test.js @@ -0,0 +1,25 @@ +import BlockchainTransaction from '../BlockchainTransaction'; + +describe('BlockchainTransaction', () => { + it('should create transaction with all provided fields', () => { + const transaction = new BlockchainTransaction({ + from: 'alice', + to: 'bob', + amount: 50, + description: 'Dinner split', + }); + expect(transaction.from).toBe('alice'); + expect(transaction.to).toBe('bob'); + expect(transaction.amount).toBe(50); + expect(transaction.description).toBe('Dinner split'); + }); + + it('should default description to an empty string', () => { + const transaction = new BlockchainTransaction({ + from: 'alice', + to: 'bob', + amount: 50, + }); + expect(transaction.description).toBe(''); + }); +}); diff --git a/src/data-structures/blockchain/images/blockchain.jpg b/src/data-structures/blockchain/images/blockchain.jpg new file mode 100644 index 0000000000..0b627eb125 Binary files /dev/null and b/src/data-structures/blockchain/images/blockchain.jpg differ