src/main/java/io/codepace/cozy/LedgerManager.java

Summary

Maintainability
D
2 days
Test Coverage
package io.codepace.cozy;

import javax.xml.bind.DatatypeConverter;
import java.io.File;
import java.io.PrintWriter;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.concurrent.ConcurrentHashMap;

public class LedgerManager {
    private File addressDatabase;
    public String addressDatabaseName;
    private ConcurrentHashMap<String, Long> addressBalances;
    private ConcurrentHashMap<String, Integer> addressSignatureCounts;
    private ArrayList<String> addresses;
    private MerkleAddressUtility merkleAddressUtility = new MerkleAddressUtility();
    public int lastBlockNum = -1;

    /**
     * Constructor for LedgerManager. All that is needed is the path to the address database file.
     * <p>
     * All peers on the network should have an identical copy of the LedgerManager object at any given time.
     * Due to latency and whatnot, that doesn't happen, but a fully synchronized network would have the same ledger on every node at any time.
     *
     * @param addressDatabase The String representation of the address database file
     */
    public LedgerManager(String addressDatabase) {
        this.addressDatabaseName = addressDatabase;
        this.addressDatabase = new File(addressDatabase);
        this.addresses = new ArrayList<>();
        addressBalances = new ConcurrentHashMap<String, Long>(16384);
        addressSignatureCounts = new ConcurrentHashMap<String, Integer>(16384);
        if (this.addressDatabase.exists()) {
            try {
                Scanner readAddressDatabase = new Scanner(this.addressDatabase);
                this.lastBlockNum = Integer.parseInt(readAddressDatabase.nextLine());
                while (readAddressDatabase.hasNextLine()) {
                    String input = readAddressDatabase.nextLine();
                    if (input.contains(":")) {
                        String[] parts = input.split(":");
                        String address = parts[0];
                        if (merkleAddressUtility.isAddressFormattedCorrectly(address)) {
                            try {
                                long addressBalance = Long.parseLong(parts[1]);
                                int currentSignatureCount = Integer.parseInt(parts[2]);
                                addressBalances.put(address, addressBalance);
                                addressSignatureCounts.put(address, currentSignatureCount);
                                addresses.add(address);
                            } catch (Exception e) {
                                System.out.println("[CRITICAL ERROR] parsing line \"" + input + "\"!");
                                e.printStackTrace();
                            }
                        }
                    }
                }
                readAddressDatabase.close();
            } catch (Exception e) {
                System.out.println("[CRITICAL ERROR] Unable to read addressDatabase file!");
                e.printStackTrace();
                System.exit(-1);
            }
        } else {
            File f = new File(addressDatabase);
            try {
                PrintWriter out = new PrintWriter(f);
                out.println("-1");
                out.close();
                this.lastBlockNum = -1; //Just in case...? Shouldn't be required.
            } catch (Exception e) {
                System.out.println("[CRITICAL ERROR] UNABLE TO WRITE LEDGER RECORD FILE!");
                e.printStackTrace();
                System.exit(-1);
            }
            System.out.println("Address Database \"" + addressDatabase + "\" does not exist! Creating...");
        }
    }

    /**
     * Hashes the entire ledger, to compare against blocks.
     *
     * @return HEX SHA256 hash of the ledger
     */
    public String getLedgerHash() {
        String ledger = "";
        for (int i = 0; i < addresses.size(); i++) {
            ledger += addresses.get(i) + ":" + addressBalances.get(addresses.get(i)) + ":" + addressSignatureCounts.get(addresses.get(i)) + "\n";
        }
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            return DatatypeConverter.printHexBinary(md.digest(ledger.getBytes("UTF-8")));
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("[CRITICAL ERROR] Unable to generate hash of ledger! Exiting...");
            System.exit(-1);
        }
        return null;
    }

    /**
     * Sets the last block num.
     *
     * @param lastBlockNum The latest block applied to this tree
     */
    public void setLastBlockNum(int lastBlockNum) {
        this.lastBlockNum = lastBlockNum;
    }

    /**
     * This method executes a given transaction String of the format InputAddress;InputAmount;OutputAddress1;OutputAmount1;OutputAddress2;OutputAmount2...;SignatureData;SignatureIndex
     *
     * @param transaction String-formatted transaction to execute
     * @return boolean Whether execution of the transaction was successful
     */
    public boolean executeTransaction(String transaction) {
        try {
            String[] transactionParts = transaction.split(";");
            String transactionMessage = "";
            for (int i = 0; i < transactionParts.length - 2; i++) {
                transactionMessage += transactionParts[i] + ";";
            }
            transactionMessage = transactionMessage.substring(0, transactionMessage.length() - 1);
            String sourceAddress = transactionParts[0];
            String signatureData = transactionParts[transactionParts.length - 2];
            long signatureIndex = Long.parseLong(transactionParts[transactionParts.length - 1]);
            if (!merkleAddressUtility.verifyMerkleSignature(transactionMessage, signatureData, sourceAddress, signatureIndex)) {
                return false; //Signature does not sign transaction message!
            }
            if (getAddressSignatureCount(sourceAddress) + 1 != signatureIndex) {
                return false; //The signature is valid, however it isn't using the expected signatureIndex. Blocked to ensure a compromised Lamport key from a previous transaction can't be used.
            }
            if (!merkleAddressUtility.isAddressFormattedCorrectly(sourceAddress)) {
                return false; //Incorrect sending address
            }
            long sourceAmount = Long.parseLong(transactionParts[1]);
            if (getAddressBalance(sourceAddress) < sourceAmount) //sourceAddress has an insufficient balance
            {
                return false; //Insufficient balance
            }
            ArrayList<String> destinationAddresses = new ArrayList<>();
            ArrayList<Long> destinationAmounts = new ArrayList<>();
            for (int i = 2; i < transactionParts.length - 2; i += 2) //-2 because last two parts of transaction are the signature and signature index
            {
                destinationAddresses.add(transactionParts[i]);
                destinationAmounts.add(Long.parseLong(transactionParts[i + 1]));
            }
            if (destinationAddresses.size() != destinationAmounts.size()) {
                return false; //This should never happen. But if it does...
            }
            for (int i = 0; i < destinationAddresses.size(); i++) {
                if (!merkleAddressUtility.isAddressFormattedCorrectly(destinationAddresses.get(i))) {
                    return false; //A destination address is not a valid address
                }
            }
            long outputTotal = 0L;
            for (int i = 0; i < destinationAmounts.size(); i++) {
                outputTotal += destinationAmounts.get(i);
            }
            if (sourceAmount < outputTotal) {
                return false;
            }
            //Looks like everything is correct--transaction should be executed correctly
            addressBalances.put(sourceAddress, getAddressBalance(sourceAddress) - sourceAmount);
            for (int i = 0; i < destinationAddresses.size(); i++) {
                addressBalances.put(destinationAddresses.get(i), getAddressBalance(destinationAddresses.get(i)) + destinationAmounts.get(i));
            }
            adjustAddressSignatureCount(sourceAddress, 1);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * This method reverse-executes a given transaction String of the format InputAddress;InputAmount;OutputAddress1;OutputAmount1;OutputAddress2;OutputAmount2...;SignatureData;SignatureIndex
     * Used primarily when a blockchain fork is resolved, and transactions have to be reversed that existed in the now-forked block(s).
     *
     * @param transaction String-formatted transaction to execute
     * @return boolean Whether execution of the transaction was successful
     */
    public boolean reverseTransaction(String transaction) {
        try {
            String[] transactionParts = transaction.split(";");
            String transactionMessage = "";
            for (int i = 0; i < transactionParts.length - 2; i++) {
                transactionMessage += transactionParts[i] + ";";
            }
            transactionMessage = transactionMessage.substring(0, transactionMessage.length() - 1);
            String sourceAddress = transactionParts[0];
            String signatureData = transactionParts[transactionParts.length - 2];
            long signatureIndex = Long.parseLong(transactionParts[transactionParts.length - 1]);
            if (!merkleAddressUtility.verifyMerkleSignature(transactionMessage, signatureData, sourceAddress, signatureIndex)) {
                return false; //Signature does not sign transaction message!
            }
            if (getAddressSignatureCount(sourceAddress) + 1 != signatureIndex) {
                //We're not concerned with this when reversing!
                //return false; //The signature is valid, however it isn't using the expected signatureIndex. Blocked to ensure a compromised Lamport key from a previous transaction can't be used.
            }
            if (!merkleAddressUtility.isAddressFormattedCorrectly(sourceAddress)) {
                return false; //Incorrect sending address
            }
            long sourceAmount = Long.parseLong(transactionParts[1]);
            ArrayList<String> destinationAddresses = new ArrayList<>();
            ArrayList<Long> destinationAmounts = new ArrayList<>();
            for (int i = 2; i < transactionParts.length - 2; i += 2) //-2 because last two parts of transaction are the signature and signature index
            {
                destinationAddresses.add(transactionParts[i]);
                destinationAmounts.add(Long.parseLong(transactionParts[i + 1]));
            }
            if (destinationAddresses.size() != destinationAmounts.size()) {
                return false; //This should never happen. But if it does...
            }
            for (int i = 0; i < destinationAddresses.size(); i++) {
                if (!merkleAddressUtility.isAddressFormattedCorrectly(destinationAddresses.get(i))) {
                    return false; //A destination address is not a valid address
                }
            }
            long outputTotal = 0L;
            for (int i = 0; i < destinationAmounts.size(); i++) {
                outputTotal += destinationAmounts.get(i);
            }
            if (sourceAmount < outputTotal) {
                return false;
            }
            for (int i = 0; i < destinationAmounts.size(); i++) {
                if (getAddressBalance(destinationAddresses.get(i)) < destinationAmounts.get(i)) {
                    System.out.println("[CRITICAL ERROR] ADDRESS " + destinationAddresses.get(i) + " needs to return " + destinationAmounts.get(i) + " but only has " + getAddressBalance(destinationAddresses.get(i))); //BIG PROBLEM THIS SHOULD NEVER HAPPEN
                    return false; //One of the addresses has an insufficient balance to reverse!
                }
            }
            //Looks like everything is correct--transaction should be reversed correctly
            addressBalances.put(sourceAddress, getAddressBalance(sourceAddress) + sourceAmount);
            for (int i = 0; i < destinationAddresses.size(); i++) {
                addressBalances.put(destinationAddresses.get(i), getAddressBalance(destinationAddresses.get(i)) - destinationAmounts.get(i));
            }
            adjustAddressSignatureCount(sourceAddress, -1);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * Writes ledger to file.
     *
     * @return boolean Whether writing the ledger to the disk was successful.
     */
    public boolean writeToFile() {
        try {
            PrintWriter out = new PrintWriter(addressDatabase);
            for (int i = 0; i < addresses.size(); i++) {
                out.println(addresses.get(i) + ":" + addressBalances.get(addresses.get(i)) + ":" + addressSignatureCounts.get(addresses.get(i)));
            }
            out.close();
        } catch (Exception e) {
            System.out.println("[CRITICAL ERROR] UNABLE TO WRITE DB FILE!");
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * Returns the last-used signature index of an address.
     *
     * @param address Address to retrieve the latest index for
     * @return int Last signature index used by address
     */
    public int getAddressSignatureCount(String address) {
        if (addressSignatureCounts.containsKey(address)) {
            return addressSignatureCounts.get(address);
        } else {
            return -1;
        }
    }

    /**
     * Adjusts an address's signature count.
     *
     * @param address    Address to adjust
     * @param adjustment Amount to adjust address's signature count by. This can be negative.
     * @return boolean Whether the adjustment was successful
     */
    public boolean adjustAddressSignatureCount(String address, int adjustment) {
        int oldCount = getAddressSignatureCount(address);
        if (oldCount + adjustment < 0) //Adjustment is negative with an absolute value larger than oldBalance
        {
            return false;
        }
        return updateAddressSignatureCount(address, oldCount + adjustment);
    }

    /**
     * Updates an address's signature count.
     *
     * @param address  Address to update
     * @param newCount New signature index to use
     * @return boolean Whether the adjustment was successful
     */
    public boolean updateAddressSignatureCount(String address, int newCount) {
        try {
            if (addressSignatureCounts.containsKey(address)) {
                addressSignatureCounts.put(address, newCount);
            } else {
                addressBalances.put(address, 0L);
                addressSignatureCounts.put(address, newCount);
                addresses.add(address);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * Returns the address balance for a given address.
     *
     * @param address Address to check balance of
     * @return long Balance of address
     */
    public long getAddressBalance(String address) {
        if (addressBalances.containsKey(address)) {
            return addressBalances.get(address);
        } else {
            return 0L;
        }
    }

    /**
     * Adjusts the balance of an address by a given adjustment, which can be positive or negative.
     *
     * @param address    Address to adjust the balance of
     * @param adjustment Amount to adjust account balance by
     * @return boolean Whether the adjustment was successful
     */
    public boolean adjustAddressBalance(String address, long adjustment) {
        long oldBalance = getAddressBalance(address);
        if (oldBalance + adjustment < 0) //Adjustment is negative with an absolute value larger than oldBalance
        {
            return false;
        }
        return updateAddressBalance(address, oldBalance + adjustment);
    }

    /**
     * Updates the balance of an address to a new amount
     *
     * @param address   Address to set the balance of
     * @param newAmount New amount to set as the balance of address
     * @return boolean Whether setting the new balance was successful
     */
    public boolean updateAddressBalance(String address, long newAmount) {
        try {
            if (addressBalances.containsKey(address)) {
                addressBalances.put(address, newAmount);
            } else {
                addressBalances.put(address, newAmount);
                addressSignatureCounts.put(address, 0);
                addresses.add(address);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}