Skip to content

Instantly share code, notes, and snippets.

@kushti
Last active August 29, 2015 14:11
Show Gist options
  • Save kushti/605dfe4f17d0dd75cedf to your computer and use it in GitHub Desktop.
Save kushti/605dfe4f17d0dd75cedf to your computer and use it in GitHub Desktop.
QORA's BlockGenerator rewritten into Scala language
package qora
import java.math.BigDecimal
import java.math.BigInteger
import ntp.NTP
import qora.account.PrivateKeyAccount
import qora.block.Block
import qora.block.BlockFactory
import qora.crypto.Crypto
import qora.transaction.Transaction
import com.google.common.primitives.Bytes
import com.google.common.primitives.Longs
import controller.Controller
import database.DBSet
import scala.collection.JavaConversions._
import scala.collection.concurrent.TrieMap
/**
* Scala version of QORA's BlockGenerator
*
* There's one behavioral difference from original logic, please see comments to the
* addUnconfirmedTransactions method. But it should be compatible with QORA.
*
*
* kushti
*/
// TODO: make code more functional, i.e. get off of mutable variables, while(true) etc
object BlockGenerator extends Thread {
val RETARGET = 10
val MIN_BALANCE = 1L
val MAX_BALANCE = 10000000000L
val MIN_BLOCK_TIME = 1 * 60
val MAX_BLOCK_TIME = 5 * 60
private val blocks = TrieMap[PrivateKeyAccount, Block]()
private var solvingBlock: Block = _
def addUnconfirmedTransaction(transaction: Transaction): Unit =
addUnconfirmedTransaction(DBSet.getInstance(), transaction)
def addUnconfirmedTransaction(db: DBSet, transaction: Transaction): Unit =
db.getTransactionMap.add(transaction)
def getUnconfirmedTransactions = DBSet.getInstance().getTransactionMap.getValues.toSeq
private def getKnownAccounts = Controller.getInstance().getPrivateKeyAccounts
override def 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) {
val lastBlockSignature = DBSet.getInstance().getBlockMap.getLastBlockSignature
//CHECK IF DIFFERENT FOR CURRENT SOLVING BLOCK
if (this.solvingBlock == null || !this.solvingBlock.getSignature.sameElements(lastBlockSignature)) {
//SET NEW BLOCK TO SOLVE
this.solvingBlock = DBSet.getInstance().getBlockMap.getLastBlock
//RESET BLOCKS
this.blocks.clear()
}
//GENERATE NEW BLOCKS
if (Controller.getInstance().doesWalletExists()) {
this.getKnownAccounts foreach { account =>
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
blocks += account -> this.generateNextBlock(DBSet.getInstance(), account, this.solvingBlock)
}
}
}
}
//IS VALID BLOCK FOUND?
val validBlockFound = this.blocks.keySet.foldLeft(false) {case (found, account) =>
val block = this.blocks.get(account).get
if (!found && block.getTimestamp <= NTP.getTime) {
//ADD TRANSACTIONS
this.addUnconfirmedTransactions(DBSet.getInstance(), block)
//ADD TRANSACTION SIGNATURE
block.setTransactionsSignature(this.calculateTransactionsSignature(block, account))
//PASS BLOCK TO CONTROLLER
Controller.getInstance().newBlockGenerated(block)
true
} else false
}
if (!validBlockFound) Thread.sleep(100)
} else {
Thread.sleep(100)
}
}
}
def generateNextBlock(db: DBSet, account: PrivateKeyAccount, block: Block) = {
//CHECK IF ACCOUNT HAS BALANCE - but already checked before call (kushti)
require (account.getGeneratingBalance(db) != BigDecimal.ZERO, "Zero balance in generateNextBlock")
val signature = this.calculateSignature(db, block, account)
val hash = Crypto.getInstance().digest(signature)
val hashValue = new BigInteger(1, hash)
//CALCULATE ACCOUNT TARGET
val targetBytes = Array.fill(32)(Byte.MaxValue)
val baseTarget = BigInteger.valueOf(getBaseTarget(getNextBlockGeneratingBalance(db, block)))
val target = new BigInteger(1, targetBytes)
.divide(baseTarget)
.multiply(account.getGeneratingBalance(db).toBigInteger) //MULTIPLY TARGET BY USER BALANCE
//CALCULATE GUESSES
val guesses = hashValue.divide(target).add(BigInteger.ONE)
//CALCULATE TIMESTAMP
val timestampRaw = guesses.multiply(BigInteger.valueOf(1000)).add(BigInteger.valueOf(block.getTimestamp))
//CHECK IF NOT HIGHER THAN MAX LONG VALUE
val timestamp = (if (timestampRaw.compareTo(BigInteger.valueOf(Long.MaxValue)) == 1)
BigInteger.valueOf(Long.MaxValue)
else timestampRaw).longValue()
val version = 1
BlockFactory.getInstance().create(version, block.getSignature, timestamp, getNextBlockGeneratingBalance(db, block), account, signature)
}
def calculateSignature(db: DBSet, solvingBlock: Block, account: PrivateKeyAccount) = {
//WRITE PARENT GENERATOR SIGNATURE
val generatorSignature = Bytes.ensureCapacity(solvingBlock.getGeneratorSignature, Block.GENERATOR_SIGNATURE_LENGTH, 0)
//WRITE GENERATING BALANCE
val baseTargetBytesRaw = Longs.toByteArray(getNextBlockGeneratingBalance(db, solvingBlock))
val baseTargetBytes = Bytes.ensureCapacity(baseTargetBytesRaw, Block.GENERATING_BALANCE_LENGTH, 0)
//WRITE GENERATOR
val generatorBytes = Bytes.ensureCapacity(account.getPublicKey, Block.GENERATOR_LENGTH, 0)
//CALC SIGNATURE OF NEWBLOCKHEADER
Crypto.getInstance().sign(account, Bytes.concat(generatorSignature, baseTargetBytes, generatorBytes))
}
def calculateTransactionsSignature(block: Block, account: PrivateKeyAccount) = {
val data = block.getTransactions.foldLeft(block.getGeneratorSignature) { case (bytes, tx) =>
Bytes.concat(bytes, tx.getSignature);
}
Crypto.getInstance().sign(account, data)
}
def addUnconfirmedTransactions(db: DBSet, block: Block) = {
//CREATE FORK OF GIVEN DATABASE
val newBlockDb = db.fork()
//ORDER TRANSACTIONS BY FEE PER BYTE
val orderedTransactions = db.getTransactionMap.getValues.toSeq.sortBy(_.feePerByte())
/* warning: simplification here!
QORA does break after first transaction matched conditions then repeat cycle
(while orderedTransactions contains transactions to process)
*/
orderedTransactions.foldLeft(0) { case (totalBytes, tx) =>
if (tx.getTimestamp <= block.getTimestamp && tx.getDeadline > block.getTimestamp
&& tx.isValid(newBlockDb) == Transaction.VALIDATE_OKE
&& totalBytes + tx.getDataLength <= Block.MAX_TRANSACTION_BYTES) {
block.addTransaction(tx)
tx.process(newBlockDb)
totalBytes + tx.getDataLength
} else totalBytes
}
}
def getNextBlockGeneratingBalance(db: DBSet, block: Block) = {
if (block.getHeight(db) % RETARGET == 0) {
//GET FIRST BLOCK OF TARGET
val firstBlock = (1 to RETARGET - 1).foldLeft(block) { case (bl, _) => bl.getParent(db)}
//CALCULATE THE GENERATING TIME FOR LAST 10 BLOCKS
val generatingTime = block.getTimestamp - firstBlock.getTimestamp
//CALCULATE EXPECTED FORGING TIME
val expectedGeneratingTime = getBlockTime(block.getGeneratingBalance) * RETARGET * 1000
//CALCULATE MULTIPLIER
val multiplier = expectedGeneratingTime / generatingTime.toDouble
//CALCULATE NEW GENERATING BALANCE
val generatingBalance = (block.getGeneratingBalance * multiplier).toLong
minMaxBalance(generatingBalance)
} else block.getGeneratingBalance
}
def getBaseTarget(generatingBalance: Long) = minMaxBalance(generatingBalance) * getBlockTime(generatingBalance)
def getBlockTime(generatingBalance: Long) = {
val percentageOfTotal = minMaxBalance(generatingBalance) / MAX_BALANCE.toDouble
(MIN_BLOCK_TIME + ((MAX_BLOCK_TIME - MIN_BLOCK_TIME) * (1 - percentageOfTotal))).toLong
}
def minMaxBalance(generatingBalance: Long) =
if (generatingBalance < MIN_BALANCE) MIN_BALANCE
else if (generatingBalance > MAX_BALANCE) MAX_BALANCE
else generatingBalance
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment