src/main/java/io/codepace/cozy/db/Blockchain.java

Summary

Maintainability
F
5 days
Test Coverage
package io.codepace.cozy.db;

import io.codepace.cozy.LedgerManager;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.ArrayList;

import static io.codepace.cozy.Util.*;

/**
 * Only one Blockchain object is created per instance of the daemon. It keeps track of ALL possible chains, and internally handles chain reorganization.
 * The decision to put LedgerManager as an object owned by Blockchain was for purposes of simplicity--there's only one Blockchain, and only one LedgerManager.
 * The Blockchain has fork management integrated, and should be able to appropriately handle any unexpected circumstances. Fork management is one of
 * the major things I'm watching for during 0.2.0. If you want to try to fork the network, please do. I'm sure there are ways.
 * <p>
 * As the blockchain has the most up-to-date information about blockchain data, it makes perfect sense for the ledger, which is based purely on the blockchain,
 * to be managed by the Blockchain object. Initial plans were to have separate Blockchain objects for each fork in a chainbut the overhead of cloning Blockchain
 * objects seemed unwarranted. Significant optimization still needs to be done on the fork management--climbing all the way down the shorter chain and back up
 * the longer chain is NOT a permanant solution and should be improved.
 * <p>
 * Additionally, there is no need to store identical blocks between multiple forks. The overhead of a fork suddenly requiring double the blockchain storage is
 * unacceptable. It works, I think. But unacceptable for production code, so that'll certainly change.
 * <p>
 * As blocks are added to the blockchain, the ledger is updated. While working beautifully in small-scale testing, I'm sure the signature count synchronization
 * between signed transactions and blocks will trip up at some point, and send the Blockchain object into either a loop of dispair, or an irrecoverable error.
 * Either is equally frightening.
 * <p>
 * You'll notice a loop which appears to retry transactions up to 10,000 times. Transactions must be executed in order--two transactions from the same address
 * need to go in order of signature index, otherwise the storage space required to maintain all used signature indexes would be astronomical.
 * <p>
 * As a caveat to the Merkle Tree signature scheme, Lamport Key reuse creates the potential for double-spend attacks, so once a Lamport Keypair is used, the network
 * rejects all future signatures from that keypair. Important to keep this in mind--
 */
public class Blockchain {
    ArrayList<ArrayList<Block>> chains = new ArrayList<>();

    private ArrayList<Block> blockQueue;

    public LedgerManager ledgerManager;

    private String dbFolder;

    private boolean gotGenesisBlock = false;

    /**
     * Constructor for Blockchain object. A Blockchain object represents an entire chain of blocks. Only one is created
     * in the entire execution of the program. All blocks will be added individually and in-order.
     * <p>
     * This blockchain object will handle all forks--it keeps a record of forks for ten blocks. After a fork falls more
     * than 10 blocks behind, it is deleted.
     * <p>
     * Blocks are stored in a 2D ArrayList, where one dimension is the blockchain 'version' (forks), and the other dimension is the blocks.
     *
     * @param dbFolder Folder for database to be contained inside
     */
    public Blockchain(String dbFolder) {
        this.dbFolder = dbFolder;
        this.ledgerManager = new LedgerManager(dbFolder + "/AccountBalances.bal");
        this.blockQueue = new ArrayList<>();
    }

    /**
     * Returns the length of the tallest tree
     *
     * @return int Length of longest tree in blockchain
     */
    public int getBlockchainLength() {
        int longestChain = 0;
        for (int i = 0; i < chains.size(); i++) {
            if (chains.get(i).size() > longestChain) {
                longestChain = chains.get(i).size();
            }
        }
        return longestChain;
    }

    /**
     * Returns the difficulty of the latest block in the longest chain.
     *
     * @return long Curreny difficulty on longest chain
     */
    public long getDifficulty() {
        int longestChainIndex = -1;
        int longestChainSize = -1;
        for (int i = 0; i < chains.size(); i++) {
            if (chains.get(i).size() > longestChainSize) {
                longestChainSize = chains.get(i).size();
                longestChainIndex = i;
            }
        }
        return chains.get(longestChainIndex).get(longestChainSize - 1).difficulty;
    }

    /**
     * This method is called periodically to attempt to add all queued blocks to the blockchain.
     */
    public void tryBlockQueue() {
        boolean addedABlock = false;
        do //Some blocks in the queue may be attempted before other dependency blocks, so while we are able to add blocks, we will continue to add them.
        {
            addedABlock = false;
            for (int i = 0; i < blockQueue.size(); i++) {
                if (blockQueue.get(i).validateBlock(this)) {
                    if (addBlock(blockQueue.get(i), false)) {
                        addedABlock = true;
                        blockQueue.remove(i);
                        i--; //Compensate for changing ArrayList size, don't want to skip an element!
                    }
                } else {
                    blockQueue.remove(i);
                }
            }
        } while (addedABlock);
    }

    /**
     * Retrieves the block at blockNum (starting from 0) from the longest chain.
     *
     * @param blockNum The block number to retrieve
     * @return Block Block at blockNum in longest chain
     */
    public Block getBlock(int blockNum) {
        int longestChainLength = 0;
        int longestChain = -1;
        for (int i = 0; i < chains.size(); i++) {
            if (chains.get(i).size() > longestChainLength) {
                longestChain = i;
                longestChainLength = chains.get(i).size();
            }
        }
        return chains.get(longestChain).get(blockNum);
    }

    /**
     * This method attempts to add a block to the blockchain. No upstream handling is required to make sure the block is valid,
     * all of that is handled here. Additionally, the block will be automatically placed onto the correct fork, or a new fork will
     * be made if necessary.
     *
     * @param block              Block to add to the blockchain
     * @param fromBlockchainFile Whether the added block was read in from file; aka !fromBlockchainFile is whether to write it back out to the db.
     * @return boolean Whether adding the block was unsuccessful. Most common source of returning false is a block that doesn't verify.
     */
    public boolean addBlock(Block block, boolean fromBlockchainFile) {
        System.out.println("Attempting to add block " + block.blockNum + " with hash " + block.blockHash);
        try {
            boolean isPOS = false;
            if (block.difficulty == 100000) // 0.2.0 Hard-coded PoS difficulty.
            {
                isPOS = true;
            }
            if (block.difficulty != 150000 && !isPOS) //0.2.0 Hard-coded PoW difficulty. 1 in 150000 nonces will win. So a certificate with 15,000 nonces has a 10% chance of winning, etc.
            {
                System.out.println("Block detected with wrong difficulty");
                return false;
            }
            //A bit of cleanup--remove chains that are more than 10 blocks shorter than the largest chain.
            int largestChainLength = 0;
            ArrayList<Block> largestChain = new ArrayList<>();
            String largestChainLastBlockHash = "";
            for (int i = 0; i < chains.size(); i++) {
                if (chains.get(i).size() > largestChainLength) {
                    largestChain = chains.get(i);
                    largestChainLength = chains.get(i).size();
                    largestChainLastBlockHash = chains.get(i).get(chains.get(i).size() - 1).blockHash;
                }
            }
            //Now we have the longest chain, we remove any chains that are less than largestChainLength - 10 in length.
            for (int i = 0; i < chains.size(); i++) {
                if (chains.get(i).size() < largestChainLength - 10) {
                    chains.remove(i);
                    i--; //Compensate for resized ArrayList!
                }
            }
            if (!block.validateBlock(this)) {
                System.out.println("Block validation failed!");
                return false; //Block is not a valid block. Don't add it!
            }
            //Block looks fine on its own--we don't know how it's going to play with the chain. If the block's number is larger than the largest chain + 1, we'll put the block in a queue to attempt to add later.
            //Block numbering starts at 0.
            if (block.blockNum > largestChainLength) {
                //Add it to the queue.
                blockQueue.add(block);
                /*
                 * In the future, the addBlock() method may be changed to return an int, with values representing things like block above existing heights, validation error, block not on any chains, etc.
                 * For now, the boolean indicates simply whether immediate addition of the block to some internal blockchain was successful.
                 */
                System.out.println("Block " + block.blockNum + " with starting hash " + block.blockHash.substring(0, 20) + " added to queue...");
                System.out.println("LargestChainLength: " + largestChainLength);
                System.out.println("block.blockNum: " + block.blockNum);
                return false; //Block wasn't added onto any chain (yet)
            }
            //If no chains exist and this is the first block, we'll create our first chain:
            if (!gotGenesisBlock) {
                gotGenesisBlock = true;
                chains.add(new ArrayList<>());
                chains.get(0).add(block);
                largestChain = chains.get(0);
                largestChainLastBlockHash = block.blockHash;
                if (ledgerManager.lastBlockNum < 0) {
                    //We can't directly assign transactionsToApply to block.transactions as we are going to edit it, and we don't want to delete transactions from the actual block.
                    ArrayList<String> transactionsToApply = new ArrayList<>(block.transactions);
                    int loopCount = 0;
                    int transactionsApplied = 0;
                    //Yippee let's add our first chunk of transactions if we need to!
                    while (transactionsToApply.size() > transactionsApplied && !transactionsToApply.get(0).equals("")) {
                        loopCount++;
                        for (int k = 0; k < transactionsToApply.size(); k++) {
                            if (ledgerManager.executeTransaction(transactionsToApply.get(k))) {
                                transactionsToApply.remove(k); //Executed correctly
                                k--; //Compensate for changed ArrayList size
                            }
                        }
                        if (loopCount > 10000) {
                            System.out.println("Infinite block detected! Hash: " + block.blockHash + " and height: " + block.blockNum);
                            System.out.println(transactionsToApply.size());
                            System.exit(-1);
                        }
                    }
                }

                ledgerManager.adjustAddressBalance(chains.get(0).get(0).certificate.redeemAddress, 100); //Pay mining fee
                ledgerManager.adjustAddressSignatureCount(chains.get(0).get(0).certificate.redeemAddress, 1);
                if (!fromBlockchainFile) {
                    writeBlockToFile(block);
                }
                return true;
            }
            //Initially, check for duplicate blocks
            for (ArrayList<Block> chain1 : chains) {
                if (chain1.get(chain1.size() - 1).blockHash.equals(block.blockHash)) {
                    //Duplicate block; block has already been added. This happens all the time, as multiple peers can all broadcast the same block.
                    System.out.println(ANSI_CYAN + "[p2p/node] " + ANSI_RESET + "- Duplicate block received from peer");
                    return false;
                }
            }
            //Then, we will see whether it goes well onto the ends of any existing chains.
            for (ArrayList<Block> chain : chains) {
                //Block numbering starts at 0
                System.out.println(ANSI_YELLOW + "[node] " + ANSI_RESET + "- Previous block hash according to chain: " + chain.get(chain.size() - 1).blockHash);
                System.out.println(ANSI_YELLOW + "[node] " + ANSI_RESET + "- Previous block hash according to added block: " + block.previousBlockHash);
                System.out.println(ANSI_YELLOW + "[node] " + ANSI_RESET + "- Selected chain size: " + chain.size());
                System.out.println(ANSI_YELLOW + "[node] " + ANSI_RESET + "- Should be equal to block num: " + block.blockNum);
                if (chain.get(chain.size() - 1).blockHash.equals(block.previousBlockHash) && chain.size() == block.blockNum) //Great! New block stacks nicely onto one of the other blocks.
                {
                    chain.add(block);
                    //We might have created a longer fork! We need to check.
                    if (chain.size() > largestChainLength) //We just created a longer chain--but it might be the original that we added to!
                    {
                        if (!chain.get(chain.size() - 2).blockHash.equals(largestChainLastBlockHash)) //Then we didn't stack onto the correct chain... We need to reverse some blocks in the ledger
                        {
                            //Future implementations will be MUCH more efficient--they'll reverse down the fork and ride it back up.
                            //However, during the developmental time squeeze that is two hours before 0.2.01 launch when I realized the logic I had here wasn't good, this seemed like a great idea.
                            for (int j = largestChain.size() - 1; j > 0; j--) {
                                ArrayList<String> transactionsToReverse = largestChain.get(j).transactions;
                                for (String aTransactionsToReverse : transactionsToReverse) {
                                    ledgerManager.reverseTransaction(aTransactionsToReverse);
                                }
                                ledgerManager.adjustAddressBalance(largestChain.get(j).certificate.redeemAddress, -100); //Reverse mining income...
                                ledgerManager.adjustAddressSignatureCount(largestChain.get(j).certificate.redeemAddress, -1);
                            }
                            //The ledger is completely empty, basically. Good job. Efficiency at its finest. Will be fixed during upcoming refactoring.
                            for (Block aChain : chain) {
                                //We can't directly assign transactionsToApply to block.transactions as we are going to edit it, and we don't want to delete transactions from the actual block.
                                ArrayList<String> transactionsToApply = new ArrayList<>(block.transactions);
                                int loopCount = 0;
                                while (transactionsToApply.size() > 0) {
                                    loopCount++;
                                    for (int k = 0; k < transactionsToApply.size(); k++) {
                                        System.out.println(ANSI_YELLOW + "[node] " + ANSI_RESET + "- Attempting to execute transaction: " + transactionsToApply.get(k).substring(0, 45) + "..." + transactionsToApply.get(k).substring(transactionsToApply.get(k).length() - 20));
                                        if (ledgerManager.executeTransaction(transactionsToApply.get(k))) {
                                            System.out.println(ANSI_YELLOW + "[node] " + ANSI_RESET + "- Successfully executed transaction!");
                                            transactionsToApply.remove(k); //Executed correctly
                                            k--; //Compensate for changed ArrayList size
                                        } else {
                                            System.out.println(ANSI_RED + "[node] " + ANSI_RESET + "- Didn't execute transaction...");
                                        }
                                    }
                                    if (loopCount > 10000) {
                                        System.out.println(ANSI_RED + "[node] " + ANSI_RESET + "- Infinite block detected! Hash: " + aChain.blockHash + " and height: " + aChain.blockNum);
                                        System.exit(-1);
                                    }
                                }
                                ledgerManager.adjustAddressBalance(aChain.certificate.redeemAddress, 100); //Pay mining fee
                                ledgerManager.adjustAddressSignatureCount(aChain.certificate.redeemAddress, 1);
                            }
                        } else //Great, we added to the longest chain!
                        {
                            //We need to execute all the transactions...
                            if (ledgerManager.lastBlockNum < block.blockNum) //If the ledger was read in from a file, then we don't add the transactions again!
                            {
                                //We can't directly assign transactionsToApply to block.transactions as we are going to edit it, and we don't want to delete transactions from the actual block.
                                ArrayList<String> transactionsToApply = new ArrayList<>(block.transactions);
                                int loopCount = 0;
                                int completedTransactions = 0;
                                while (transactionsToApply.size() > completedTransactions) {
                                    loopCount++;
                                    for (int k = 0; k < transactionsToApply.size(); k++) {
                                        if (ledgerManager.executeTransaction(transactionsToApply.get(k))) {
                                            transactionsToApply.remove(k); //Executed correctly
                                            k--; //Compensate for changed ArrayList size
                                        } else if (transactionsToApply.get(k).equals("")) {
                                            completedTransactions++;
                                        }
                                    }
                                    if (loopCount > 10000) {
                                        System.out.println(ANSI_RED + "[node] " + ANSI_RESET + "- Infinite block detected! Hash: " + block.blockHash + " and height: " + block.blockNum);
                                        System.exit(-1);
                                    }
                                }
                                ledgerManager.adjustAddressBalance(block.certificate.redeemAddress, 100);
                                ledgerManager.adjustAddressSignatureCount(block.certificate.redeemAddress, 1);
                            }
                        }
                    }
                    if (!fromBlockchainFile) {
                        writeBlockToFile(block);
                    }
                    return true;
                } else {
                    System.out.println("Something went wrong with stacking...");
                    System.out.println(chain.get(chain.size() - 1).blockHash);
                    System.out.println(block.previousBlockHash);
                }
            }
            boolean foundPlaceForBlock = false;
            for (int i = 0; i < chains.size(); i++) {
                ArrayList<Block> tempChain = chains.get(i); //No working with this ArrayList indirectly; we've got a lot of work to do!
                for (int j = tempChain.size() - 11; j < tempChain.size(); j++) {
                    if (j < 0) //Pathetically-early network forks handled
                    {
                        j = 0; //Negative blocks aren't one of the features of 2.0
                    }
                    if (tempChain.get(j).blockHash.equals(block.previousBlockHash)) //Found where it forked!
                    {
                        ArrayList<Block> newChain = new ArrayList<>();
                        //Add all of the old chain until we get to the fork
                        for (int k = 0; k <= j; k++) {
                            newChain.add(tempChain.get(k));
                        }
                        //Add the fork block, which is an alternative to tempChain.get(j + 1)
                        newChain.add(block);
                        foundPlaceForBlock = true;
                        j = tempChain.size();
                        i = chains.size();
                        //As block didn't fit onto the end of another chain, we don't need to check for the new fork being longer.
                    }
                }
            }
            if (foundPlaceForBlock) //Was put on a fork. We need to make sure that the dominant blockchain is represented by the Ledger.
            {
                //Might be stuff here in the future...
            } else {
                //Didn't fit on any existing blockchain. Probably really old.
                System.out.println("Block didn't fit! :(");
                return false;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (!fromBlockchainFile) {
            writeBlockToFile(block);
        }
        return true;
    }

    /**
     * Writes a block to the blockchain file
     *
     * @param block The block object to write
     * @return boolean Whether write was successful
     */
    public boolean writeBlockToFile(Block block) {
        System.out.println("Writing a block to file...");
        try (FileWriter fileWriter = new FileWriter(dbFolder + "/blockchain.dta", true);
             BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
             PrintWriter out = new PrintWriter(bufferedWriter)) {
            out.println(block.getRawBlock());
        } catch (Exception e) {
            System.out.println(ANSI_RED + "[node] " + ANSI_RESET + "- ERROR: UNABLE TO SAVE BLOCK TO DATABASE!");
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * Saves entire blockchain to a file, useful to save the state of the blockchain so it doesn't have to be redownloaded later.
     * Blockchain is stored to a file called "blockchain.dta" inside the provided dbFolder.
     *
     * @param dbFolder Folder to save blockchain file in
     * @return boolean Whether saving to file was successful.
     */
    public boolean saveToFile(String dbFolder) {
        try {
            PrintWriter out = new PrintWriter(new File(dbFolder + "/blockchain.dta"));
            for (ArrayList<Block> chain : chains) {
                for (int j = 0; j < chain.size(); j++) {
                    out.println(chain.get(j).getRawBlock());
                }
            }
            out.close();
        } catch (Exception e) {
            System.out.println(ANSI_RED + "[node] " + ANSI_RESET + "- [CRITICAL ERROR] UNABLE TO WRITE BLOCKCHAIN FILE \"" + dbFolder + "/blockchain.dta!");
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * Calls getTransactionsInvolvingAddress() on all Block objects in the current Blockchain to get all relevant transactions.
     *
     * @param addressToFind Address to search through all block transaction pools for
     * @return {@link ArrayList} All transactions in simplified form blocknum:sender:amount:receiver of
     */
    public ArrayList<String> getAllTransactionsInvolvingAddress(String addressToFind) {
        int longestChainLength = 0;
        int longestChainNum = -1;
        for (int i = 0; i < chains.size(); i++) {
            if (chains.get(i).size() > longestChainLength) {
                longestChainNum = i;
                longestChainLength = chains.get(i).size();
            }
        }
        ArrayList<Block> longestChain = chains.get(longestChainNum);

        ArrayList<String> allTransactions = new ArrayList<>();

        for (int i = 0; i < longestChain.size(); i++) {
            ArrayList<String> transactionsFromBlock = longestChain.get(i).getTransactionsInvolvingAddress(addressToFind);
            for (int j = 0; j < transactionsFromBlock.size(); j++) {
                allTransactions.add(longestChain.get(i).blockNum + ":" + transactionsFromBlock.get(j));
            }
        }
        return allTransactions;
    }

    /**
     * Passthrough method to the LedgerManager object.
     *
     * @param address Address to check balance of
     * @return long Balance of address
     */
    public long getAddressBalance(String address) {
        return ledgerManager.getAddressBalance(address);
    }
}