package qora.block;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import ntp.NTP;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import qora.BlockGenerator;
import qora.account.PublicKeyAccount;
import qora.crypto.Base58;
import qora.crypto.Crypto;
import qora.transaction.GenesisTransaction;
import qora.transaction.Transaction;
import qora.transaction.TransactionFactory;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import database.DBSet;
public class Block {
public static final int MAX_BLOCK_BYTES = 1048576;
public static final int VERSION_LENGTH = 4;
public static final int REFERENCE_LENGTH = 128;
public static final int TIMESTAMP_LENGTH = 8;
public static final int GENERATING_BALANCE_LENGTH = 8;
public static final int GENERATOR_LENGTH = 32;
public static final int GENERATOR_SIGNATURE_LENGTH = 64;
private static final int TRANSACTIONS_SIGNATURE_LENGTH = 64;
private static final int TRANSACTIONS_COUNT_LENGTH = 4;
private static final int TRANSACTION_SIZE_LENGTH = 4;
private static final int BASE_LENGTH = VERSION_LENGTH + REFERENCE_LENGTH + TIMESTAMP_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH + GENERATOR_SIGNATURE_LENGTH + TRANSACTIONS_COUNT_LENGTH;
public static final int MAX_TRANSACTION_BYTES = MAX_BLOCK_BYTES - BASE_LENGTH;
protected int version;
protected byte[] reference;
protected long timestamp;
protected long generatingBalance;
protected PublicKeyAccount generator;
protected byte[] generatorSignature;
private List<Transaction> transactions;
private int transactionCount;
private byte[] rawTransactions;
protected byte[] transactionsSignature;
public Block(int version, byte[] reference, long timestamp, long generatingBalance, PublicKeyAccount generator, byte[] generatorSignature)
{
this.version = version;
this.reference = reference;
this.timestamp = timestamp;
this.generatingBalance = generatingBalance;
this.generator = generator;
this.generatorSignature = generatorSignature;
this.transactionCount = 0;
}
//GETTERS/SETTERS
public int getVersion()
{
return version;
}
public byte[] getGeneratorSignature()
{
return this.generatorSignature;
}
public long getTimestamp()
{
return this.timestamp;
}
public long getGeneratingBalance()
{
return this.generatingBalance;
}
public byte[] getReference()
{
return this.reference;
}
public PublicKeyAccount getGenerator()
{
return this.generator;
}
public BigDecimal getTotalFee()
{
BigDecimal fee = BigDecimal.ZERO.setScale(8);
for(Transaction transaction: this.getTransactions())
{
fee = fee.add(transaction.getFee());
}
return fee;
}
public void setTransactionData(int transactionCount, byte[] rawTransactions)
{
this.transactionCount = transactionCount;
this.rawTransactions = rawTransactions;
}
public int getTransactionCount()
{
return this.transactionCount;
}
public synchronized List<Transaction> getTransactions()
{
if(this.transactions == null)
{
//LOAD TRANSACTIONS
this.transactions = new ArrayList<Transaction>();
try
{
int position = 0;
for(int i=0; i<transactionCount; i++)
{
//GET TRANSACTION SIZE
byte[] transactionLengthBytes = Arrays.copyOfRange(this.rawTransactions, position, position + TRANSACTION_SIZE_LENGTH);
int transactionLength = Ints.fromByteArray(transactionLengthBytes);
//PARSE TRANSACTION
byte[] transactionBytes = Arrays.copyOfRange(this.rawTransactions, position + TRANSACTION_SIZE_LENGTH, position + TRANSACTION_SIZE_LENGTH + transactionLength);
Transaction transaction = TransactionFactory.getInstance().parse(transactionBytes);
//ADD TO TRANSACTIONS
this.transactions.add(transaction);
//ADD TO POSITION
position += TRANSACTION_SIZE_LENGTH + transactionLength;
}
}
catch(Exception e)
{
//FAILED TO LOAD TRANSACTIONS
}
}
return this.transactions;
}
public void addTransaction(Transaction transaction)
{
this.getTransactions().add(transaction);
this.transactionCount++;
}
public Transaction getTransaction(byte[] signature) {
for(Transaction transaction: this.getTransactions())
{
if(Arrays.equals(transaction.getSignature(), signature))
{
return transaction;
}
}
return null;
}
public Block getParent()
{
return this.getParent(DBSet.getInstance());
}
public Block getParent(DBSet db)
{
return db.getBlockMap().get(this.reference);
}
public Block getChild()
{
return this.getChild(DBSet.getInstance());
}
public Block getChild(DBSet db)
{
return db.getChildMap().get(this);
}
public int getHeight()
{
return this.getHeight(DBSet.getInstance());
}
public int getHeight(DBSet db)
{
return db.getHeightMap().get(this);
}
public void setTransactionsSignature(byte[] transactionsSignature)
{
this.transactionsSignature = transactionsSignature;
}
public byte[] getSignature()
{
return Bytes.concat(this.generatorSignature, this.transactionsSignature);
}
//PARSE/CONVERT
public static Block parse(byte[] data) throws Exception
{
//CHECK IF WE HAVE MINIMUM BLOCK LENGTH
if(data.length < BASE_LENGTH)
{
throw new Exception("Data is less then minimum block length");
}
int position = 0;
//READ VERSION
byte[] versionBytes = Arrays.copyOfRange(data, position, position + VERSION_LENGTH);
int version = Ints.fromByteArray(versionBytes);
position += VERSION_LENGTH;
//READ TIMESTAMP
byte[] timestampBytes = Arrays.copyOfRange(data, position, position + TIMESTAMP_LENGTH);
long timestamp = Longs.fromByteArray(timestampBytes);
position += TIMESTAMP_LENGTH;
//READ REFERENCE
byte[] reference = Arrays.copyOfRange(data, position, position + REFERENCE_LENGTH);
position += REFERENCE_LENGTH;
//READ GENERATING BALANCE
byte[] generatingBalanceBytes = Arrays.copyOfRange(data, position, position + GENERATING_BALANCE_LENGTH);
long generatingBalance = Longs.fromByteArray(generatingBalanceBytes);
position += GENERATING_BALANCE_LENGTH;
//READ GENERATOR
byte[] generatorBytes = Arrays.copyOfRange(data, position, position + GENERATOR_LENGTH);
PublicKeyAccount generator = new PublicKeyAccount(generatorBytes);
position += GENERATOR_LENGTH;
//READ TRANSACTION SIGNATURE
byte[] transactionsSignature = Arrays.copyOfRange(data, position, position + TRANSACTIONS_SIGNATURE_LENGTH);
position += TRANSACTIONS_SIGNATURE_LENGTH;
//READ GENERATOR SIGNATURE
byte[] generatorSignature = Arrays.copyOfRange(data, position, position + GENERATOR_SIGNATURE_LENGTH);
position += GENERATOR_SIGNATURE_LENGTH;
//CREATE BLOCK
Block block = new Block(version, reference, timestamp, generatingBalance, generator, generatorSignature);
//READ TRANSACTIONS COUNT
byte[] transactionCountBytes = Arrays.copyOfRange(data, position, position + TRANSACTIONS_COUNT_LENGTH);
int transactionCount = Ints.fromByteArray(transactionCountBytes);
position += TRANSACTIONS_COUNT_LENGTH;
//SET TRANSACTIONDATA
byte[] rawTransactions = Arrays.copyOfRange(data, position, data.length);
block.setTransactionData(transactionCount, rawTransactions);
//SET TRANSACTIONS SIGNATURE
block.setTransactionsSignature(transactionsSignature);
return block;
}
@SuppressWarnings("unchecked")
public JSONObject toJson()
{
JSONObject block = new JSONObject();
block.put("version", this.version);
block.put("reference", Base58.encode(this.reference));
block.put("timestamp", this.timestamp);
block.put("generatingBalance", this.generatingBalance);
block.put("generator", this.generator.getAddress());
block.put("fee", this.getTotalFee().toPlainString());
block.put("transactionsSignature", Base58.encode(this.transactionsSignature));
block.put("generatorSignature", Base58.encode(this.generatorSignature));
block.put("signature", Base58.encode(this.getSignature()));
//CREATE TRANSACTIONS
JSONArray transactionsArray = new JSONArray();
for(Transaction transaction: this.getTransactions())
{
transactionsArray.add(transaction.toJson());
}
//ADD TRANSACTIONS TO BLOCK
block.put("transactions", transactionsArray);
//RETURN
return block;
}
public byte[] toBytes()
{
byte[] data = new byte[0];
//WRITE VERSION
byte[] versionBytes = Ints.toByteArray(this.version);
//versionBytes = Bytes.ensureCapacity(versionBytes, 4, 0);
data = Bytes.concat(data, versionBytes);
//WRITE TIMESTAMP
byte[] timestampBytes = Longs.toByteArray(this.timestamp);
timestampBytes = Bytes.ensureCapacity(timestampBytes, 8, 0);
data = Bytes.concat(data, timestampBytes);
//WRITE REFERENCE
byte[] referenceBytes = Bytes.ensureCapacity(this.reference, REFERENCE_LENGTH, 0);
data = Bytes.concat(data, referenceBytes);
//WRITE GENERATING BALANCE
byte[] baseTargetBytes = Longs.toByteArray(this.generatingBalance);
//baseTargetBytes = Bytes.ensureCapacity(baseTargetBytes, 8, 0);
data = Bytes.concat(data,baseTargetBytes);
//WRITE GENERATOR
byte[] generatorBytes = Bytes.ensureCapacity(this.generator.getPublicKey(), GENERATOR_LENGTH, 0);
data = Bytes.concat(data, generatorBytes);
//WRITE TRANSACTIONS SIGNATURE
data = Bytes.concat(data, this.transactionsSignature);
//WRITE GENERATOR SIGNATURE
data = Bytes.concat(data, this.generatorSignature);
//WRITE TRANSACTION COUNT
byte[] transactionCountBytes = Ints.toByteArray(this.getTransactionCount());
//transactionCountBytes = Bytes.ensureCapacity(transactionCountBytes, 4, 0);
data = Bytes.concat(data, transactionCountBytes);
for(Transaction transaction: this.getTransactions())
{
//WRITE TRANSACTION LENGTH
int transactionLength = transaction.getDataLength();
byte[] transactionLengthBytes = Ints.toByteArray(transactionLength);
//transactionLengthBytes = Bytes.ensureCapacity(transactionLengthBytes, 4, 0);
data = Bytes.concat(data, transactionLengthBytes);
//WRITE TRANSACTION
data = Bytes.concat(data, transaction.toBytes());
}
return data;
}
public int getDataLength() {
int length = BASE_LENGTH;
for(Transaction transaction: this.getTransactions())
{
length += 4 + transaction.getDataLength();
}
return length;
}
//VALIDATE
public boolean isSignatureValid()
{
//VALIDATE BLOCK SIGNATURE
byte[] data = new byte[0];
//WRITE PARENT GENERATOR SIGNATURE
byte[] generatorSignature = Arrays.copyOfRange(this.reference, 0, GENERATOR_SIGNATURE_LENGTH);
data = Bytes.concat(data, generatorSignature);
//WRITE GENERATING BALANCE
byte[] baseTargetBytes = Longs.toByteArray(this.generatingBalance);
data = Bytes.concat(data, baseTargetBytes);
//WRITE GENERATOR
byte[] generatorBytes = Bytes.ensureCapacity(this.generator.getPublicKey(), GENERATOR_LENGTH, 0);
data = Bytes.concat(data, generatorBytes);
if(!Crypto.getInstance().verify(this.generator.getPublicKey(), this.generatorSignature, data))
{
return false;
}
//VALIDATE TRANSACTIONS SIGNATURE
data = this.generatorSignature;
for(Transaction transaction: this.getTransactions())
{
//CHECK IF TRANSACTION SIGNATURE IS VALID
if(!transaction.isSignatureValid())
{
return false;
}
//ADD SIGNATURE TO DATA
data = Bytes.concat(data, transaction.getSignature());
}
if(!Crypto.getInstance().verify(this.generator.getPublicKey(), this.transactionsSignature, data))
{
return false;
}
return true;
}
public boolean isValid()
{
return this.isValid(DBSet.getInstance());
}
public boolean isValid(DBSet db)
{
//CHECK IF PARENT EXISTS
if(this.reference == null || this.getParent(db) == null)
{
return false;
}
//CHECK IF TIMESTAMP IS VALID -500 MS ERROR MARGIN TIME
if(this.timestamp - 500 > NTP.getTime() || this.timestamp < this.getParent(db).timestamp)
{
return false;
}
//CHECK IF TIMESTAMP REST SAME AS PARENT TIMESTAMP REST
if(this.timestamp % 1000 != this.getParent(db).timestamp % 1000)
{
return false;
}
//CHECK IF GENERATING BALANCE IS CORRECT
if(this.generatingBalance != BlockGenerator.getNextBlockGeneratingBalance(db, this.getParent(db)))
{
return false;
}
//CREATE 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(BlockGenerator.getBaseTarget(this.generatingBalance));
target = target.divide(baseTarget);
//MULTIPLY TARGET BY USER BALANCE
target = target.multiply(this.generator.getGeneratingBalance(db).toBigInteger());
//MULTIPLE TARGET BY GUESSES
long guesses = (this.timestamp - this.getParent(db).getTimestamp()) / 1000;
BigInteger lowerTarget = target.multiply(BigInteger.valueOf(guesses-1));
target = target.multiply(BigInteger.valueOf(guesses));
//HASH SIGNATURE
byte[] hash = Crypto.getInstance().digest(this.generatorSignature);
//CONVERT HASH TO BIGINT
BigInteger hashValue = new BigInteger(1, hash);
//CHECK IF HASH LOWER THEN TARGET
if(hashValue.compareTo(target) >= 0)
{
return false;
}
//CHECK IF FIRST BLOCK OF USER
if(hashValue.compareTo(lowerTarget) < 0)
{
return false;
}
//CHECK TRANSACTIONS
DBSet fork = db.fork();
for(Transaction transaction: this.getTransactions())
{
//CHECK IF NOT GENESISTRANSACTION
if(transaction instanceof GenesisTransaction)
{
return false;
}
//CHECK IF VALID
if(transaction.isValid(fork) != Transaction.VALIDATE_OKE)
{
return false;
}
//CHECK TIMESTAMP AND DEADLINE
if(transaction.getTimestamp() > this.timestamp || transaction.getDeadline() <= this.timestamp)
{
return false;
}
//PROCESS TRANSACTION IN MEMORYDB TO MAKE SURE OTHER TRANSACTIONS VALIDATE PROPERLY
transaction.process(fork);
}
//BLOCK IS VALID
return true;
}
//PROCESS/ORPHAN
public void process()
{
this.process(DBSet.getInstance());
}
public void process(DBSet db)
{
//PROCESS TRANSACTIONS
for(Transaction transaction: this.getTransactions())
{
//PROCESS
transaction.process(db);
//SET PARENT
db.getTransactionParentMap().set(transaction, this);
//REMOVE FROM UNCONFIRMED DATABASE
db.getTransactionMap().delete(transaction);
}
//PROCESS FEE
BigDecimal blockFee = this.getTotalFee();
if(blockFee.compareTo(BigDecimal.ZERO) == 1)
{
//UPDATE GENERATOR BALANCE WITH FEE
this.generator.setConfirmedBalance(this.generator.getConfirmedBalance(db).add(blockFee), db);
}
Block parent = this.getParent(db);
if(parent != null)
{
//SET AS CHILD OF PARENT
db.getChildMap().set(parent, this);
//SET BLOCK HEIGHT
int height = parent.getHeight(db) + 1;
db.getHeightMap().set(this, height);
}
else
{
//IF NO PARENT HEIGHT IS 1
db.getHeightMap().set(this, 1);
}
//ADD TO DB
db.getBlockMap().add(this);
//UPDATE LAST BLOCK
db.getBlockMap().setLastBlock(this);
}
public void orphan()
{
this.orphan(DBSet.getInstance());
}
public void orphan(DBSet db)
{
//ORPHAN TRANSACTIONS
this.orphanTransactions(this.getTransactions(), db);
//REMOVE FEE
BigDecimal blockFee = this.getTotalFee();
if(blockFee.compareTo(BigDecimal.ZERO) == 1)
{
//UPDATE GENERATOR BALANCE WITH FEE
this.generator.setConfirmedBalance(this.generator.getConfirmedBalance(db).subtract(blockFee), db);
}
//DELETE BLOCK FROM DB
db.getBlockMap().delete(this);
//SET PARENT AS LAST BLOCK
db.getBlockMap().setLastBlock(this.getParent(db));
//ADD ORPHANED TRANASCTIONS BACK TO DATABASE
for(Transaction transaction: this.getTransactions())
{
db.getTransactionMap().add(transaction);
}
}
private void orphanTransactions(List<Transaction> transactions, DBSet db)
{
//ORPHAN ALL TRANSACTIONS IN DB BACK TO FRONT
for(int i=transactions.size() -1; i>=0; i--)
{
Transaction transaction = transactions.get(i);
transaction.orphan(db);
}
}
}