package qora;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ntp.NTP;
import qora.account.PrivateKeyAccount;
import qora.block.Block;
import qora.block.BlockFactory;
import qora.crypto.Crypto;
import qora.transaction.Transaction;
import settings.Settings;
import utils.TransactionFeeComparator;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Longs;
import controller.Controller;
import database.DBSet;
public class BlockGenerator extends Thread
{
public static final int RETARGET = 10;
public static final long MIN_BALANCE = 1l;
public static final long MAX_BALANCE = 10000000000l;
public static final int MIN_BLOCK_TIME = 1 * 60;
public static final int MAX_BLOCK_TIME = 5 * 60;
private Map<PrivateKeyAccount, Block> blocks;
private Block solvingBlock;
private List<PrivateKeyAccount> cachedAccounts;
public BlockGenerator()
{
if(Settings.getInstance().isGeneratorKeyCachingEnabled())
{
this.cachedAccounts = new ArrayList<PrivateKeyAccount>();
}
}
public void addUnconfirmedTransaction(Transaction transaction)
{
this.addUnconfirmedTransaction(DBSet.getInstance(), transaction, true);
}
public void addUnconfirmedTransaction(DBSet db, Transaction transaction, boolean process)
{
//ADD TO TRANSACTION DATABASE
db.getTransactionMap().add(transaction);
}
public List<Transaction> getUnconfirmedTransactions()
{
return new ArrayList<Transaction>(DBSet.getInstance().getTransactionMap().getValues());
}
private List<PrivateKeyAccount> getKnownAccounts()
{
//CHECK IF CACHING ENABLED
if(Settings.getInstance().isGeneratorKeyCachingEnabled())
{
List<PrivateKeyAccount> privateKeyAccounts = Controller.getInstance().getPrivateKeyAccounts();
//IF ACCOUNTS EXISTS
if(privateKeyAccounts.size() > 0)
{
//CACHE ACCOUNTS
this.cachedAccounts = privateKeyAccounts;
}
//RETURN CACHED ACCOUNTS
return this.cachedAccounts;
}
else
{
//RETURN ACCOUNTS
return Controller.getInstance().getPrivateKeyAccounts();
}
}
public void run()
{
while(true)
{
//CHECK IF WE ARE UPTODATE
if(!Controller.getInstance().isUpToDate())
{
Controller.getInstance().update();
}
//CHECK IF WE HAVE CONNECTIONS
if(Controller.getInstance().getStatus() == Controller.STATUS_OKE)
{
//GET LAST BLOCK
byte[] lastBlockSignature = DBSet.getInstance().getBlockMap().getLastBlockSignature();
//CHECK IF DIFFERENT FOR CURRENT SOLVING BLOCK
if(this.solvingBlock == null || !Arrays.equals(this.solvingBlock.getSignature(), lastBlockSignature))
{
//SET NEW BLOCK TO SOLVE
this.solvingBlock = DBSet.getInstance().getBlockMap().getLastBlock();
//RESET BLOCKS
this.blocks = new HashMap<PrivateKeyAccount, Block>();
}
//GENERATE NEW BLOCKS
if(Controller.getInstance().doesWalletExists() /*&& Controller.getInstance().isWalletUnlocked()*/)
{
//PREVENT CONCURRENT MODIFY EXCEPTION
List<PrivateKeyAccount> knownAccounts = this.getKnownAccounts();
synchronized(knownAccounts)
{
for(PrivateKeyAccount account: knownAccounts)
{
if(account.getGeneratingBalance().compareTo(BigDecimal.ONE) >= 0)
{
//CHECK IF BLOCK FROM USER ALREADY EXISTS USE MAP ACCOUNT BLOCK EASY
if(!this.blocks.containsKey(account))
{
//GENERATE NEW BLOCK FOR USER
this.blocks.put(account, this.generateNextBlock(DBSet.getInstance(), account, this.solvingBlock));
}
}
}
}
}
//VALID BLOCK FOUND
boolean validBlockFound = false;
//CHECK IF BLOCK IS VALID
for(PrivateKeyAccount account: this.blocks.keySet())
{
Block block = this.blocks.get(account);
/*Date date = new Date(block.getTimestamp());
DateFormat format = DateFormat.getDateTimeInstance();
System.out.println(format.format(date));*/
//CHECK IF BLACK TIMESTAMP IS VALID
if(block.getTimestamp() <= NTP.getTime() && !validBlockFound)
{
//ADD TRANSACTIONS
this.addUnconfirmedTransactions(DBSet.getInstance(), block);
//ADD TRANSACTION SIGNATURE
block.setTransactionsSignature(this.calculateTransactionsSignature(block, account));
//PASS BLOCK TO CONTROLLER
Controller.getInstance().newBlockGenerated(block);
//BLOCK FOUND
validBlockFound = true;
}
}
//IF NO BLOCK FOUND
if(!validBlockFound)
{
//SLEEP
try
{
Thread.sleep(100);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
else
{
//SLEEP
try
{
Thread.sleep(100);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
public Block generateNextBlock(DBSet db, PrivateKeyAccount account, Block block)
{
//CHECK IF ACCOUNT HAS BALANCE
if(account.getGeneratingBalance(db) == BigDecimal.ZERO)
{
return null;
}
//CALCULATE SIGNATURE
byte[] signature = this.calculateSignature(db, block, account);
//CALCULATE HASH
byte[] hash = Crypto.getInstance().digest(signature);
//CONVERT HASH TO BIGINT
BigInteger hashValue = new BigInteger(1, hash);
//CALCULATE ACCOUNT TARGET
byte[] targetBytes = new byte[32];
Arrays.fill(targetBytes, Byte.MAX_VALUE);
BigInteger target = new BigInteger(1, targetBytes);
//DIVIDE TARGET BY BASE TARGET
BigInteger baseTarget = BigInteger.valueOf(getBaseTarget(getNextBlockGeneratingBalance(db, block)));
target = target.divide(baseTarget);
//MULTIPLY TARGET BY USER BALANCE
target = target.multiply(account.getGeneratingBalance(db).toBigInteger());
//CALCULATE GUESSES
//long guesses = hashValue.divide(target).longValue() + 1;
BigInteger guesses = hashValue.divide(target).add(BigInteger.ONE);
//CALCULATE TIMESTAMP
//long timestamp = block.getTimestamp() + (guesses * 1000);
BigInteger timestamp = guesses.multiply(BigInteger.valueOf(1000)).add(BigInteger.valueOf(block.getTimestamp()));
//CHECK IF NOT HIGHER THAN MAX LONG VALUE
if(timestamp.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) == 1)
{
timestamp = BigInteger.valueOf(Long.MAX_VALUE);
}
//CREATE NEW BLOCK
int version = 1;
Block newBlock = BlockFactory.getInstance().create(version, block.getSignature(), timestamp.longValue(), getNextBlockGeneratingBalance(db, block), account, signature);
return newBlock;
}
private byte[] calculateSignature(DBSet db, Block solvingBlock, PrivateKeyAccount account)
{
byte[] data = new byte[0];
//WRITE PARENT GENERATOR SIGNATURE
byte[] generatorSignature = Bytes.ensureCapacity(solvingBlock.getGeneratorSignature(), Block.GENERATOR_SIGNATURE_LENGTH, 0);
data = Bytes.concat(data, generatorSignature);
//WRITE GENERATING BALANCE
byte[] baseTargetBytes = Longs.toByteArray(getNextBlockGeneratingBalance(db, solvingBlock));
baseTargetBytes = Bytes.ensureCapacity(baseTargetBytes, Block.GENERATING_BALANCE_LENGTH, 0);
data = Bytes.concat(data,baseTargetBytes);
//WRITE GENERATOR
byte[] generatorBytes = Bytes.ensureCapacity(account.getPublicKey(), Block.GENERATOR_LENGTH, 0);
data = Bytes.concat(data, generatorBytes);
//CALC SIGNATURE OF NEWBLOCKHEADER
byte[] signature = Crypto.getInstance().sign(account, data);
return signature;
}
public byte[] calculateTransactionsSignature(Block block, PrivateKeyAccount account)
{
byte[] data = block.getGeneratorSignature();
//WRITE TRANSACTION SIGNATURE
for(Transaction transaction: block.getTransactions())
{
data = Bytes.concat(data, transaction.getSignature());
}
return Crypto.getInstance().sign(account, data);
}
public void addUnconfirmedTransactions(DBSet db, Block block)
{
long totalBytes = 0;
boolean transactionProcessed;
//CREATE FORK OF GIVEN DATABASE
DBSet newBlockDb = db.fork();
//ORDER TRANSACTIONS BY FEE PER BYTE
List<Transaction> orderedTransactions = new ArrayList<Transaction>(db.getTransactionMap().getValues());
Collections.sort(orderedTransactions, new TransactionFeeComparator());
//Collections.sort(orderedTransactions, Collections.reverseOrder());
do
{
transactionProcessed = false;
for(Transaction transaction: orderedTransactions)
{
//CHECK TRANSACTION TIMESTAMP AND DEADLINE
if(transaction.getTimestamp() <= block.getTimestamp() && transaction.getDeadline() > block.getTimestamp())
{
//CHECK IF VALID
if(transaction.isValid(newBlockDb) == Transaction.VALIDATE_OKE)
{
//CHECK IF ENOUGH ROOM
if(totalBytes + transaction.getDataLength() <= Block.MAX_TRANSACTION_BYTES)
{
//ADD INTO BLOCK
block.addTransaction(transaction);
//REMOVE FROM LIST
orderedTransactions.remove(transaction);
//PROCESS IN NEWBLOCKDB
transaction.process(newBlockDb);
//TRANSACTION PROCESSES
transactionProcessed = true;
break;
}
}
}
}
}
while(transactionProcessed == true);
}
/*public void addObserver(Observer o)
{
o.update(null, new ObserverMessage(ObserverMessage.LIST_TRANSACTION_TYPE, DBSet.getInstance().getTransactionMap().getValues()));
}*/
public static long getNextBlockGeneratingBalance(DBSet db, Block block)
{
int height = block.getHeight(db);
if(height % RETARGET == 0)
{
//CALCULATE THE GENERATING TIME FOR LAST 10 BLOCKS
long generatingTime = block.getTimestamp();
//GET FIRST BLOCK OF TARGET
Block firstBlock = block;
for(int i=1; i<RETARGET; i++)
{
firstBlock = firstBlock.getParent(db);
}
generatingTime -= firstBlock.getTimestamp();
//CALCULATE EXPECTED FORGING TIME
long expectedGeneratingTime = getBlockTime(block.getGeneratingBalance()) * RETARGET * 1000;
//CALCULATE MULTIPLIER
double multiplier = (double) expectedGeneratingTime / (double) generatingTime;
//CALCULATE NEW GENERATING BALANCE
long generatingBalance = (long) (block.getGeneratingBalance() * multiplier);
return minMaxBalance(generatingBalance);
}
return block.getGeneratingBalance();
}
public static long getBaseTarget(long generatingBalance)
{
generatingBalance = minMaxBalance(generatingBalance);
long baseTarget = generatingBalance * getBlockTime(generatingBalance);
return baseTarget;
}
public static long getBlockTime(long generatingBalance)
{
generatingBalance = minMaxBalance(generatingBalance);
double percentageOfTotal = (double) generatingBalance / MAX_BALANCE;
long actualBlockTime = (long) (MIN_BLOCK_TIME + ((MAX_BLOCK_TIME - MIN_BLOCK_TIME) * (1 - percentageOfTotal)));
return actualBlockTime;
}
private static long minMaxBalance(long generatingBalance)
{
if(generatingBalance < MIN_BALANCE)
{
return MIN_BALANCE;
}
if(generatingBalance > MAX_BALANCE)
{
return MAX_BALANCE;
}
return generatingBalance;
}
}