Add account level change due to blocks generated.

Removed ENABLE_FORGING and related.

Still to do: 'orphan' version of Block.increaseAccountLevels
This commit is contained in:
catbref
2019-10-10 13:52:00 +01:00
parent 54d49e0f1d
commit 319bfc8d75
17 changed files with 288 additions and 683 deletions

View File

@@ -203,24 +203,22 @@ public class Account {
this.repository.getAccountRepository().setFlags(accountData);
}
public boolean isFounder() throws DataException {
Integer flags = this.getFlags();
public static boolean isFounder(Integer flags) {
return flags != null && (flags & FOUNDER_FLAG) != 0;
}
// Forging Enabler
public boolean isFounder() throws DataException {
Integer flags = this.getFlags();
return Account.isFounder(flags);
}
// Forging
public boolean canForge() throws DataException {
Integer level = this.getLevel();
return level != null && level > 0;
}
public void setForgingEnabler(String address) throws DataException {
AccountData accountData = this.buildAccountData();
accountData.setForgingEnabler(address);
this.repository.getAccountRepository().setForgingEnabler(accountData);
}
// Account level
public Integer getLevel() throws DataException {

View File

@@ -1,20 +0,0 @@
package org.qora.account;
import org.qora.block.BlockChain;
import org.qora.repository.DataException;
import org.qora.utils.BitTwiddling;
/** Relating to whether accounts can forge. */
public class Forging {
/** Returns mask for account flags for forging bits. */
public static int getForgingMask() {
return BitTwiddling.calcMask(BlockChain.getInstance().getForgingTiers().size() - 1);
}
public static boolean canForge(Account account) throws DataException {
Integer level = account.getLevel();
return level != null && level > 0;
}
}

View File

@@ -36,7 +36,6 @@ import javax.ws.rs.core.MediaType;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.RollingFileAppender;
import org.qora.account.Forging;
import org.qora.account.PrivateKeyAccount;
import org.qora.api.ApiError;
import org.qora.api.ApiErrors;
@@ -255,8 +254,8 @@ public class AdminResource {
// Check seed is valid
PrivateKeyAccount forgingAccount = new PrivateKeyAccount(repository, seed);
// Account must derive to known proxy forging public key or have minting flag set
if (!Forging.canForge(forgingAccount) && !repository.getAccountRepository().isProxyPublicKey(forgingAccount.getPublicKey()))
// Account must derive to known proxy forging public key
if (!repository.getAccountRepository().isProxyPublicKey(forgingAccount.getPublicKey()))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
ForgingAccountData forgingAccountData = new ForgingAccountData(seed);

View File

@@ -5,7 +5,6 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -15,7 +14,6 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
@@ -30,16 +28,10 @@ import org.qora.api.model.BlockForgerSummary;
import org.qora.crypto.Crypto;
import org.qora.data.account.AccountData;
import org.qora.data.block.BlockData;
import org.qora.data.transaction.EnableForgingTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.settings.Settings;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.ValidationResult;
import org.qora.transform.TransformationException;
import org.qora.transform.transaction.EnableForgingTransactionTransformer;
import org.qora.utils.Base58;
@Path("/blocks")
@@ -506,50 +498,4 @@ public class BlocksResource {
}
}
@POST
@Path("/enableforging")
@Operation(
summary = "Build raw, unsigned, ENABLE_FORGING transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = EnableForgingTransactionData.class
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned, ENABLE_FORGING transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String enableForging(EnableForgingTransactionData transactionData) {
if (Settings.getInstance().isApiRestricted())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = EnableForgingTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@@ -11,22 +11,23 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qora.account.Account;
import org.qora.account.Forging;
import org.qora.account.PrivateKeyAccount;
import org.qora.account.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.at.AT;
import org.qora.block.BlockChain;
import org.qora.block.BlockChain.BlockTimingByHeight;
import org.qora.block.BlockChain.ShareByLevel;
import org.qora.controller.Controller;
import org.qora.crypto.Crypto;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData;
import org.qora.data.account.ProxyForgerData;
import org.qora.data.at.ATData;
import org.qora.data.at.ATStateData;
@@ -122,8 +123,77 @@ public class Block {
/** Locally-generated AT fees */
protected BigDecimal ourAtFees; // Generated locally
/** Minimum Qora balance for use in calculations. */
public static final BigDecimal MIN_BALANCE = BigDecimal.valueOf(1L).setScale(8);
/** Lazy-instantiated expanded info on block's online accounts. */
class ExpandedAccount {
final ProxyForgerData proxyForgerData;
final boolean isRecipientAlsoForger;
final Account forgerAccount;
final AccountData forgerAccountData;
final boolean isForgerFounder;
final BigDecimal forgerQoraAmount;
final int shareBin;
final Account recipientAccount;
final AccountData recipientAccountData;
final boolean isRecipientFounder;
ExpandedAccount(Repository repository, int accountIndex) throws DataException {
final List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
this.proxyForgerData = repository.getAccountRepository().getProxyAccountByIndex(accountIndex);
this.forgerAccount = new PublicKeyAccount(repository, this.proxyForgerData.getForgerPublicKey());
this.recipientAccount = new Account(repository, this.proxyForgerData.getRecipient());
AccountBalanceData qoraBalanceData = repository.getAccountRepository().getBalance(this.forgerAccount.getAddress(), Asset.LEGACY_QORA);
if (qoraBalanceData != null && qoraBalanceData.getBalance() != null && qoraBalanceData.getBalance().compareTo(BigDecimal.ZERO) > 0)
this.forgerQoraAmount = qoraBalanceData.getBalance();
else
this.forgerQoraAmount = null;
this.forgerAccountData = repository.getAccountRepository().getAccount(this.forgerAccount.getAddress());
this.isForgerFounder = Account.isFounder(forgerAccountData.getFlags());
int currentShareBin = -1;
if (!this.isForgerFounder)
for (int s = 0; s < sharesByLevel.size(); ++s)
if (sharesByLevel.get(s).levels.contains(this.forgerAccountData.getLevel())) {
currentShareBin = s;
break;
}
this.shareBin = currentShareBin;
this.recipientAccountData = repository.getAccountRepository().getAccount(this.recipientAccount.getAddress());
this.isRecipientFounder = Account.isFounder(recipientAccountData.getFlags());
this.isRecipientAlsoForger = this.forgerAccountData.getAddress().equals(this.recipientAccountData.getAddress());
}
void distribute(BigDecimal accountAmount) throws DataException {
final BigDecimal oneHundred = BigDecimal.valueOf(100L);
if (this.forgerAccount.getAddress().equals(this.recipientAccount.getAddress())) {
// forger & recipient the same - simpler case
LOGGER.trace(() -> String.format("Forger/recipient account %s share: %s", this.forgerAccount.getAddress(), accountAmount.toPlainString()));
this.forgerAccount.setConfirmedBalance(Asset.QORT, this.forgerAccount.getConfirmedBalance(Asset.QORT).add(accountAmount));
} else {
// forger & recipient different - extra work needed
BigDecimal recipientAmount = accountAmount.multiply(this.proxyForgerData.getShare()).divide(oneHundred, RoundingMode.DOWN);
BigDecimal forgerAmount = accountAmount.subtract(recipientAmount);
LOGGER.trace(() -> String.format("Forger account %s share: %s", this.forgerAccount.getAddress(), forgerAmount.toPlainString()));
this.forgerAccount.setConfirmedBalance(Asset.QORT, this.forgerAccount.getConfirmedBalance(Asset.QORT).add(forgerAmount));
LOGGER.trace(() -> String.format("Recipient account %s share: %s", this.recipientAccount.getAddress(), recipientAmount.toPlainString()));
this.recipientAccount.setConfirmedBalance(Asset.QORT, this.recipientAccount.getConfirmedBalance(Asset.QORT).add(recipientAmount));
}
}
}
/** Always use getExpandedAccounts() to access this, as it's lazy-instantiated. */
private List<ExpandedAccount> cachedExpandedAccounts = null;
// Other useful constants
@@ -440,6 +510,31 @@ public class Block {
return this.atStates;
}
/**
* Return expanded info on block's online accounts.
* <p>
* @throws DataException
*/
public List<ExpandedAccount> getExpandedAccounts() throws DataException {
if (this.cachedExpandedAccounts != null)
return this.cachedExpandedAccounts;
ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(this.blockData.getEncodedOnlineAccounts());
List<ExpandedAccount> expandedAccounts = new ArrayList<ExpandedAccount>();
IntIterator iterator = accountIndexes.iterator();
while (iterator.hasNext()) {
int accountIndex = iterator.next();
ExpandedAccount accountInfo = new ExpandedAccount(repository, accountIndex);
expandedAccounts.add(accountInfo);
}
this.cachedExpandedAccounts = expandedAccounts;
return this.cachedExpandedAccounts;
}
// Navigation
/**
@@ -1032,21 +1127,13 @@ public class Block {
/** Returns whether block's generator is actually allowed to forge this block. */
protected boolean isGeneratorValidToForge(Block parentBlock) throws DataException {
// Generator must have forging flag enabled
Account generator = new PublicKeyAccount(repository, this.blockData.getGeneratorPublicKey());
if (Forging.canForge(generator))
return true;
// Check whether generator public key could be a proxy forge account
// Block's generator public key must be known proxy forging public key
ProxyForgerData proxyForgerData = this.repository.getAccountRepository().getProxyForgeData(this.blockData.getGeneratorPublicKey());
if (proxyForgerData != null) {
Account forger = new PublicKeyAccount(this.repository, proxyForgerData.getForgerPublicKey());
if (proxyForgerData == null)
return false;
if (Forging.canForge(forger))
return true;
}
return false;
Account forger = new PublicKeyAccount(this.repository, proxyForgerData.getForgerPublicKey());
return forger.canForge();
}
/**
@@ -1059,11 +1146,13 @@ public class Block {
int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
this.blockData.setHeight(blockchainHeight + 1);
// Increase account levels
increaseAccountLevels();
if (this.blockData.getHeight() > 1) {
// Increase account levels
increaseAccountLevels();
// Block rewards go before transactions processed
processBlockRewards();
// Block rewards go before transactions processed
processBlockRewards();
}
// Process transactions (we'll link them to this block after saving the block itself)
processTransactions();
@@ -1071,8 +1160,9 @@ public class Block {
// Group-approval transactions
processGroupApprovalTransactions();
// Give transaction fees to generator/proxy
rewardTransactionFees();
if (this.blockData.getHeight() > 1)
// Give transaction fees to generator/proxy
rewardTransactionFees();
// Process AT fees and save AT states into repository
processAtFeesAndStates();
@@ -1091,7 +1181,71 @@ public class Block {
}
protected void increaseAccountLevels() throws DataException {
// TODO!
List<Integer> blocksNeededByLevel = BlockChain.getInstance().getBlocksNeededByLevel();
// Pre-calculate cumulative blocks required for each level
int cumulativeBlocks = 0;
int[] cumulativeBlocksByLevel = new int[blocksNeededByLevel.size() + 1];
for (int level = 0; level < cumulativeBlocksByLevel.length; ++level) {
cumulativeBlocksByLevel[level] = cumulativeBlocks;
if (level < blocksNeededByLevel.size())
cumulativeBlocks += blocksNeededByLevel.get(level);
}
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
// We need to do this for both forgers and recipients
this.increaseAccountLevels(expandedAccounts, cumulativeBlocksByLevel,
expandedAccount -> expandedAccount.isForgerFounder,
expandedAccount -> expandedAccount.forgerAccountData);
this.increaseAccountLevels(expandedAccounts, cumulativeBlocksByLevel,
expandedAccount -> expandedAccount.isRecipientFounder,
expandedAccount -> expandedAccount.recipientAccountData);
}
private void increaseAccountLevels(List<ExpandedAccount> expandedAccounts, int[] cumulativeBlocksByLevel,
Predicate<ExpandedAccount> isFounder, Function<ExpandedAccount, AccountData> getAccountData) throws DataException {
final boolean isProcessingRecipients = getAccountData.apply(expandedAccounts.get(0)) == expandedAccounts.get(0).recipientAccountData;
// Increase blocks generated count for all accounts
for (int a = 0; a < expandedAccounts.size(); ++a) {
ExpandedAccount expandedAccount = expandedAccounts.get(a);
// Don't increase twice if recipient is also forger.
if (isProcessingRecipients && expandedAccount.isRecipientAlsoForger)
continue;
AccountData accountData = getAccountData.apply(expandedAccount);
accountData.setBlocksGenerated(accountData.getBlocksGenerated() + 1);
repository.getAccountRepository().setBlocksGenerated(accountData);
LOGGER.trace(() -> String.format("Block generator %s has generated %d block%s", accountData.getAddress(), accountData.getBlocksGenerated(), (accountData.getBlocksGenerated() != 1 ? "s" : "")));
}
// We are only interested in accounts that are NOT founders and NOT already highest level
final int maximumLevel = cumulativeBlocksByLevel.length - 1;
List<ExpandedAccount> candidateAccounts = expandedAccounts.stream().filter(expandedAccount -> !isFounder.test(expandedAccount) && getAccountData.apply(expandedAccount).getLevel() < maximumLevel).collect(Collectors.toList());
for (int c = 0; c < candidateAccounts.size(); ++c) {
ExpandedAccount expandedAccount = candidateAccounts.get(c);
final AccountData accountData = getAccountData.apply(expandedAccount);
final int effectiveBlocksGenerated = cumulativeBlocksByLevel[accountData.getInitialLevel()] + accountData.getBlocksGenerated();
for (int newLevel = cumulativeBlocksByLevel.length - 1; newLevel > 0; --newLevel)
if (effectiveBlocksGenerated >= cumulativeBlocksByLevel[newLevel]) {
if (newLevel > accountData.getLevel()) {
// Account has increased in level!
accountData.setLevel(newLevel);
repository.getAccountRepository().setLevel(accountData);
LOGGER.trace(() -> String.format("Block generator %s bumped to level %d", accountData.getAddress(), accountData.getLevel()));
}
break;
}
}
}
protected void processBlockRewards() throws DataException {
@@ -1233,8 +1387,9 @@ public class Block {
* @throws DataException
*/
public void orphan() throws DataException {
// Deduct any transaction fees from generator/proxy
deductTransactionFees();
if (this.blockData.getHeight() > 1)
// Deduct any transaction fees from generator/proxy
deductTransactionFees();
// Orphan, and unlink, transactions from this block
orphanTransactionsFromBlock();
@@ -1242,11 +1397,13 @@ public class Block {
// Undo any group-approval decisions that happen at this block
orphanGroupApprovalTransactions();
// Block rewards removed after transactions undone
orphanBlockRewards();
if (this.blockData.getHeight() > 1) {
// Block rewards removed after transactions undone
orphanBlockRewards();
// Decrease account levels
decreaseAccountLevels();
// Decrease account levels
decreaseAccountLevels();
}
// Return AT fees and delete AT states from repository
orphanAtFeesAndStates();
@@ -1354,81 +1511,8 @@ public class Block {
}
protected void distributeByAccountLevel(BigDecimal totalAmount) throws DataException {
class AccountInfo {
final ProxyForgerData proxyForgerData;
final Account forgerAccount;
final boolean isFounder;
final int level;
final int shareBin;
final BigDecimal qoraAmount;
final Account recipientAccount;
AccountInfo(Repository repository, int accountIndex, List<ShareByLevel> sharesByLevel) throws DataException {
this.proxyForgerData = repository.getAccountRepository().getProxyAccountByIndex(accountIndex);
this.forgerAccount = new PublicKeyAccount(repository, this.proxyForgerData.getForgerPublicKey());
this.recipientAccount = new Account(repository, this.proxyForgerData.getRecipient());
AccountBalanceData qoraBalanceData = repository.getAccountRepository().getBalance(this.forgerAccount.getAddress(), Asset.LEGACY_QORA);
if (qoraBalanceData != null && qoraBalanceData.getBalance() != null && qoraBalanceData.getBalance().compareTo(BigDecimal.ZERO) > 0)
this.qoraAmount = qoraBalanceData.getBalance();
else
this.qoraAmount = null;
if (this.forgerAccount.isFounder()) {
this.isFounder = true;
this.level = 0;
this.shareBin = -1;
return;
}
this.isFounder = false;
this.level = this.forgerAccount.getLevel();
for (int s = 0; s < sharesByLevel.size(); ++s)
if (sharesByLevel.get(s).levels.contains(this.level)) {
this.shareBin = s;
return;
}
this.shareBin = -1;
}
void distribute(BigDecimal accountAmount) throws DataException {
final BigDecimal ONE_HUNDRED = BigDecimal.valueOf(100L);
Account forgerAccount = this.forgerAccount;
Account recipientAccount = this.recipientAccount;
if (forgerAccount.getAddress().equals(recipientAccount.getAddress())) {
// forger & recipient the same - simpler case
LOGGER.trace(() -> String.format("Forger/recipient account %s share: %s", forgerAccount.getAddress(), accountAmount.toPlainString()));
forgerAccount.setConfirmedBalance(Asset.QORT, forgerAccount.getConfirmedBalance(Asset.QORT).add(accountAmount));
} else {
// forger & recipient different - extra work needed
BigDecimal recipientAmount = accountAmount.multiply(this.proxyForgerData.getShare()).divide(ONE_HUNDRED, RoundingMode.DOWN);
BigDecimal forgerAmount = accountAmount.subtract(recipientAmount);
LOGGER.trace(() -> String.format("Forger account %s share: %s", forgerAccount.getAddress(), forgerAmount.toPlainString()));
forgerAccount.setConfirmedBalance(Asset.QORT, forgerAccount.getConfirmedBalance(Asset.QORT).add(forgerAmount));
LOGGER.trace(() -> String.format("Recipient account %s share: %s", recipientAccount.getAddress(), recipientAmount.toPlainString()));
recipientAccount.setConfirmedBalance(Asset.QORT, recipientAccount.getConfirmedBalance(Asset.QORT).add(recipientAmount));
}
}
}
List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(this.blockData.getEncodedOnlineAccounts());
List<AccountInfo> expandedAccounts = new ArrayList<AccountInfo>();
IntIterator iterator = accountIndexes.iterator();
while (iterator.hasNext()) {
int accountIndex = iterator.next();
AccountInfo accountInfo = new AccountInfo(repository, accountIndex, sharesByLevel);
expandedAccounts.add(accountInfo);
}
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
// Distribute amount across bins
BigDecimal sharedAmount = BigDecimal.ZERO;
@@ -1439,7 +1523,7 @@ public class Block {
LOGGER.trace(() -> String.format("Bin %d share of %s: %s", binIndex, totalAmount.toPlainString(), binAmount.toPlainString()));
// Spread across all accounts in bin
List<AccountInfo> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> !accountInfo.isFounder && accountInfo.shareBin == binIndex).collect(Collectors.toList());
List<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> !accountInfo.isForgerFounder && accountInfo.shareBin == binIndex).collect(Collectors.toList());
if (binnedAccounts.isEmpty())
continue;
@@ -1447,8 +1531,8 @@ public class Block {
BigDecimal accountAmount = binAmount.divide(binSize, RoundingMode.DOWN);
for (int a = 0; a < binnedAccounts.size(); ++a) {
AccountInfo accountInfo = binnedAccounts.get(a);
accountInfo.distribute(accountAmount);
ExpandedAccount expandedAccount = binnedAccounts.get(a);
expandedAccount.distribute(accountAmount);
sharedAmount = sharedAmount.add(accountAmount);
}
}
@@ -1457,27 +1541,27 @@ public class Block {
BigDecimal qoraHoldersAmount = BlockChain.getInstance().getQoraHoldersShare().multiply(totalAmount).setScale(8, RoundingMode.DOWN);
LOGGER.trace(() -> String.format("Legacy QORA holders share of %s: %s", totalAmount.toPlainString(), qoraHoldersAmount.toPlainString()));
List<AccountInfo> qoraHolderAccounts = new ArrayList<>();
List<ExpandedAccount> qoraHolderAccounts = new ArrayList<>();
BigDecimal totalQoraHeld = BigDecimal.ZERO;
for (int i = 0; i < expandedAccounts.size(); ++i) {
AccountInfo accountInfo = expandedAccounts.get(i);
if (accountInfo.qoraAmount == null)
ExpandedAccount expandedAccount = expandedAccounts.get(i);
if (expandedAccount.forgerQoraAmount == null)
continue;
qoraHolderAccounts.add(accountInfo);
totalQoraHeld = totalQoraHeld.add(accountInfo.qoraAmount);
qoraHolderAccounts.add(expandedAccount);
totalQoraHeld = totalQoraHeld.add(expandedAccount.forgerQoraAmount);
}
final BigDecimal finalTotalQoraHeld = totalQoraHeld;
LOGGER.trace(() -> String.format("Total legacy QORA held: %s", finalTotalQoraHeld.toPlainString()));
for (int h = 0; h < qoraHolderAccounts.size(); ++h) {
AccountInfo accountInfo = qoraHolderAccounts.get(h);
final BigDecimal holderAmount = qoraHoldersAmount.multiply(totalQoraHeld).divide(accountInfo.qoraAmount, RoundingMode.DOWN);
ExpandedAccount expandedAccount = qoraHolderAccounts.get(h);
final BigDecimal holderAmount = qoraHoldersAmount.multiply(totalQoraHeld).divide(expandedAccount.forgerQoraAmount, RoundingMode.DOWN);
LOGGER.trace(() -> String.format("Forger account %s has %s / %s QORA so share: %s",
accountInfo.forgerAccount.getAddress(), accountInfo.qoraAmount, finalTotalQoraHeld, holderAmount.toPlainString()));
expandedAccount.forgerAccount.getAddress(), expandedAccount.forgerQoraAmount, finalTotalQoraHeld, holderAmount.toPlainString()));
accountInfo.distribute(holderAmount);
expandedAccount.distribute(holderAmount);
sharedAmount = sharedAmount.add(holderAmount);
}
@@ -1485,13 +1569,16 @@ public class Block {
BigDecimal foundersAmount = totalAmount.subtract(sharedAmount);
LOGGER.debug(String.format("Shared %s of %s, remaining %s to founders", sharedAmount.toPlainString(), totalAmount.toPlainString(), foundersAmount.toPlainString()));
List<AccountInfo> founderAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.isFounder).collect(Collectors.toList());
List<ExpandedAccount> founderAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.isForgerFounder).collect(Collectors.toList());
if (founderAccounts.isEmpty())
return;
BigDecimal foundersCount = BigDecimal.valueOf(founderAccounts.size());
BigDecimal accountAmount = foundersAmount.divide(foundersCount, RoundingMode.DOWN);
for (int a = 0; a < founderAccounts.size(); ++a) {
AccountInfo accountInfo = founderAccounts.get(a);
accountInfo.distribute(accountAmount);
ExpandedAccount expandedAccount = founderAccounts.get(a);
expandedAccount.distribute(accountAmount);
sharedAmount = sharedAmount.add(accountAmount);
}
}

View File

@@ -51,6 +51,7 @@ public class BlockChain {
// Properties
private boolean isTestChain = false;
/** Transaction expiry period, starting from transaction's timestamp, in milliseconds. */
private long transactionExpiryPeriod;
@@ -109,6 +110,14 @@ public class BlockChain {
/** Share of block reward/fees to legacy QORA coin holders */
BigDecimal qoraHoldersShare;
/**
* Number of generated blocks required to reach next level.
* <p>
* Use account's current level as index.<br>
* If account's level isn't valid as an index, then account's level is at maximum.
*/
List<Integer> blocksNeededByLevel;
/** Block times by block height */
public static class BlockTimingByHeight {
public int height;
@@ -118,15 +127,6 @@ public class BlockChain {
}
List<BlockTimingByHeight> blockTimingsByHeight;
/** Forging right tiers */
public static class ForgingTier {
/** Minimum number of blocks forged before account can enable minting on other accounts. */
public int minBlocks;
/** Maximum number of other accounts that can be enabled. */
public int maxSubAccounts;
}
List<ForgingTier> forgingTiers;
private int maxProxyRelationships;
/** Minimum time to retain online account signatures (ms) for block validity checks. */
@@ -287,12 +287,12 @@ public class BlockChain {
return this.sharesByLevel;
}
public BigDecimal getQoraHoldersShare() {
return this.qoraHoldersShare;
public List<Integer> getBlocksNeededByLevel() {
return this.blocksNeededByLevel;
}
public List<ForgingTier> getForgingTiers() {
return this.forgingTiers;
public BigDecimal getQoraHoldersShare() {
return this.qoraHoldersShare;
}
public int getMaxProxyRelationships() {
@@ -378,6 +378,9 @@ public class BlockChain {
if (this.qoraHoldersShare == null)
Settings.throwValidationError("No \"qoraHoldersShare\" entry found in blockchain config");
if (this.blocksNeededByLevel == null)
Settings.throwValidationError("No \"blocksNeededByLevel\" entry found in blockchain config");
if (this.blockTimingsByHeight == null)
Settings.throwValidationError("No \"blockTimingsByHeight\" entry found in blockchain config");

View File

@@ -15,9 +15,9 @@ public class AccountData {
protected byte[] publicKey;
protected int defaultGroupId;
protected int flags;
protected String forgingEnabler;
protected int initialLevel;
protected int level;
protected int blocksGenerated;
// Constructors
@@ -25,19 +25,19 @@ public class AccountData {
protected AccountData() {
}
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, String forgingEnabler, int initialLevel, int level) {
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, int initialLevel, int level, int blocksGenerated) {
this.address = address;
this.reference = reference;
this.publicKey = publicKey;
this.defaultGroupId = defaultGroupId;
this.flags = flags;
this.forgingEnabler = forgingEnabler;
this.initialLevel = initialLevel;
this.level = level;
this.blocksGenerated = blocksGenerated;
}
public AccountData(String address) {
this(address, null, null, Group.NO_GROUP, 0, null, 0, 0);
this(address, null, null, Group.NO_GROUP, 0, 0, 0, 0);
}
// Getters/Setters
@@ -78,14 +78,6 @@ public class AccountData {
this.flags = flags;
}
public String getForgingEnabler() {
return this.forgingEnabler;
}
public void setForgingEnabler(String forgingEnabler) {
this.forgingEnabler = forgingEnabler;
}
public int getInitialLevel() {
return this.initialLevel;
}
@@ -102,6 +94,14 @@ public class AccountData {
this.level = level;
}
public int getBlocksGenerated() {
return this.blocksGenerated;
}
public void setBlocksGenerated(int blocksGenerated) {
this.blocksGenerated = blocksGenerated;
}
// Comparison
@Override

View File

@@ -75,11 +75,11 @@ public interface AccountRepository {
public void setInitialLevel(AccountData accountData) throws DataException;
/**
* Saves account's forging enabler, and public key if present, in repository.
* Saves account's generated block count and public key if present, in repository.
* <p>
* Note: ignores other fields like last reference, default groupID.
*/
public void setForgingEnabler(AccountData accountData) throws DataException;
public void setBlocksGenerated(AccountData accountData) throws DataException;
/** Delete account from repository. */
public void delete(String address) throws DataException;

View File

@@ -26,7 +26,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public AccountData getAccount(String address) throws DataException {
String sql = "SELECT reference, public_key, default_group_id, flags, forging_enabler, initial_level, level FROM Accounts WHERE account = ?";
String sql = "SELECT reference, public_key, default_group_id, flags, initial_level, level, blocks_generated FROM Accounts WHERE account = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
if (resultSet == null)
@@ -36,11 +36,11 @@ public class HSQLDBAccountRepository implements AccountRepository {
byte[] publicKey = resultSet.getBytes(2);
int defaultGroupId = resultSet.getInt(3);
int flags = resultSet.getInt(4);
String forgingEnabler = resultSet.getString(5);
int initialLevel = resultSet.getInt(6);
int level = resultSet.getInt(7);
int initialLevel = resultSet.getInt(5);
int level = resultSet.getInt(6);
int blocksGenerated = resultSet.getInt(7);
return new AccountData(address, reference, publicKey, defaultGroupId, flags, forgingEnabler, initialLevel, level);
return new AccountData(address, reference, publicKey, defaultGroupId, flags, initialLevel, level, blocksGenerated);
} catch (SQLException e) {
throw new DataException("Unable to fetch account info from repository", e);
}
@@ -236,10 +236,10 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
@Override
public void setForgingEnabler(AccountData accountData) throws DataException {
public void setBlocksGenerated(AccountData accountData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
saveHelper.bind("account", accountData.getAddress()).bind("forging_enabler", accountData.getForgingEnabler());
saveHelper.bind("account", accountData.getAddress()).bind("blocks_generated", accountData.getBlocksGenerated());
byte[] publicKey = accountData.getPublicKey();
if (publicKey != null)
@@ -248,7 +248,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save account's forging enabler into repository", e);
throw new DataException("Unable to save account's generated block count into repository", e);
}
}

View File

@@ -799,6 +799,15 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("ALTER TABLE IssueAssetTransactions ADD COLUMN is_unspendable BOOLEAN NOT NULL DEFAULT FALSE BEFORE asset_id");
break;
case 57:
// Modify accounts to keep track of how many blocks generated
stmt.execute("ALTER TABLE Accounts ADD COLUMN blocks_generated INT NOT NULL DEFAULT 0");
// Remove forging_enabler
stmt.execute("ALTER TABLE Accounts DROP COLUMN forging_enabler");
// Remove corresponding ENABLE_FORGING transaction
stmt.execute("DROP TABLE EnableForgingTransactions");
break;
default:
// nothing to do
return false;

View File

@@ -43,8 +43,7 @@ public class HSQLDBAccountLevelTransactionRepository extends HSQLDBTransactionRe
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountLevelTransactions");
saveHelper.bind("signature", accountLevelTransactionData.getSignature()).bind("creator", accountLevelTransactionData.getCreatorPublicKey())
.bind("target", accountLevelTransactionData.getTarget()).bind("level", accountLevelTransactionData.getLevel())
.bind("previous_level", accountLevelTransactionData.getPreviousLevel());
.bind("target", accountLevelTransactionData.getTarget()).bind("level", accountLevelTransactionData.getLevel());
try {
saveHelper.execute(this.repository);

View File

@@ -1,172 +0,0 @@
package org.qora.transaction;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import org.qora.account.Account;
import org.qora.account.Forging;
import org.qora.account.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.block.BlockChain;
import org.qora.block.BlockChain.ForgingTier;
import org.qora.data.transaction.EnableForgingTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
public class EnableForgingTransaction extends Transaction {
// Properties
private EnableForgingTransactionData enableForgingTransactionData;
// Constructors
public EnableForgingTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData);
this.enableForgingTransactionData = (EnableForgingTransactionData) this.transactionData;
}
// More information
@Override
public List<Account> getRecipientAccounts() throws DataException {
return Collections.emptyList();
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();
if (address.equals(this.getCreator().getAddress()))
return true;
if (address.equals(this.getTarget().getAddress()))
return true;
return false;
}
@Override
public BigDecimal getAmount(Account account) throws DataException {
String address = account.getAddress();
BigDecimal amount = BigDecimal.ZERO.setScale(8);
if (address.equals(this.getCreator().getAddress()))
amount = amount.subtract(this.transactionData.getFee());
return amount;
}
// Navigation
public Account getTarget() {
return new Account(this.repository, this.enableForgingTransactionData.getTarget());
}
// Processing
@Override
public ValidationResult isValid() throws DataException {
PublicKeyAccount creator = getCreator();
// Creator needs to have at least one forging-enabled account flag set
Integer creatorFlags = creator.getFlags();
if (creatorFlags == null)
return ValidationResult.INVALID_ADDRESS;
if ((creatorFlags & Forging.getForgingMask()) == 0)
return ValidationResult.NO_FORGING_PERMISSION;
int forgingTierLevel = 0;
ForgingTier forgingTier = null;
List<ForgingTier> forgingTiers = BlockChain.getInstance().getForgingTiers();
for (forgingTierLevel = 0; forgingTierLevel < forgingTiers.size(); ++forgingTierLevel)
if ((creatorFlags & (1 << forgingTierLevel)) != 0) {
forgingTier = forgingTiers.get(forgingTierLevel);
break;
}
// forgingTier should not be null at this point
if (forgingTier == null)
return ValidationResult.NO_FORGING_PERMISSION;
// Final tier forgers can't enable further accounts
if (forgingTierLevel == forgingTiers.size() - 1)
return ValidationResult.FORGING_ENABLE_LIMIT;
Account target = getTarget();
// Target needs to NOT have ANY forging-enabled account flags set
if (Forging.canForge(target))
return ValidationResult.FORGING_ALREADY_ENABLED;
// Has creator reached minimum requirements?
// Already gifted maximum number of forging rights?
int numberEnabledAccounts = this.repository.getAccountRepository().countForgingAccountsEnabledByAddress(creator.getAddress());
if (numberEnabledAccounts >= forgingTier.maxSubAccounts)
return ValidationResult.FORGING_ENABLE_LIMIT;
// Not enough forged blocks to gift forging rights?
int numberForgedBlocks = this.repository.getBlockRepository().countForgedBlocks(creator.getPublicKey());
if (numberForgedBlocks < forgingTier.minBlocks)
return ValidationResult.FORGE_MORE_BLOCKS;
// Check fee is zero or positive
if (enableForgingTransactionData.getFee().compareTo(BigDecimal.ZERO) < 0)
return ValidationResult.NEGATIVE_FEE;
// Check creator has enough funds
if (creator.getConfirmedBalance(Asset.QORT).compareTo(enableForgingTransactionData.getFee()) < 0)
return ValidationResult.NO_BALANCE;
return ValidationResult.OK;
}
@Override
public void process() throws DataException {
Account creator = getCreator();
int creatorFlags = creator.getFlags();
int forgeBit = creatorFlags & Forging.getForgingMask();
// Target's forging bit is next level from creator's
int targetForgeBit = forgeBit << 1;
Account target = getTarget();
Integer targetFlags = target.getFlags();
if (targetFlags == null)
targetFlags = 0;
targetFlags |= targetForgeBit;
target.setFlags(targetFlags);
target.setForgingEnabler(creator.getAddress());
}
@Override
public void orphan() throws DataException {
// Revert
Account creator = getCreator();
int creatorFlags = creator.getFlags();
int forgeBit = creatorFlags & Forging.getForgingMask();
// Target's forging bit is next level from creator's
int targetForgeBit = forgeBit << 1;
Account target = getTarget();
int targetFlags = target.getFlags();
targetFlags &= ~targetForgeBit;
target.setFlags(targetFlags);
target.setForgingEnabler(null);
}
}

View File

@@ -6,7 +6,6 @@ import java.util.Collections;
import java.util.List;
import org.qora.account.Account;
import org.qora.account.Forging;
import org.qora.account.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.block.BlockChain;
@@ -86,7 +85,7 @@ public class ProxyForgingTransaction extends Transaction {
PublicKeyAccount creator = getCreator();
// Creator themselves needs to be allowed to forge
if (!Forging.canForge(creator))
if (!creator.canForge())
return ValidationResult.NO_FORGING_PERMISSION;
// Check proxy public key is correct length

View File

@@ -1,86 +0,0 @@
package org.qora.transform.transaction;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import org.qora.block.BlockChain;
import org.qora.data.transaction.BaseTransactionData;
import org.qora.data.transaction.EnableForgingTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.transaction.Transaction.TransactionType;
import org.qora.transform.TransformationException;
import org.qora.utils.Serialization;
public class EnableForgingTransactionTransformer extends TransactionTransformer {
// Property lengths
private static final int TARGET_LENGTH = ADDRESS_LENGTH;
private static final int EXTRAS_LENGTH = TARGET_LENGTH;
protected static final TransactionLayout layout;
static {
layout = new TransactionLayout();
layout.add("txType: " + TransactionType.GROUP_INVITE.valueString, TransformationType.INT);
layout.add("timestamp", TransformationType.TIMESTAMP);
layout.add("transaction's groupID", TransformationType.INT);
layout.add("reference", TransformationType.SIGNATURE);
layout.add("account's public key", TransformationType.PUBLIC_KEY);
layout.add("target account's address", TransformationType.ADDRESS);
layout.add("fee", TransformationType.AMOUNT);
layout.add("signature", TransformationType.SIGNATURE);
}
public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
long timestamp = byteBuffer.getLong();
int txGroupId = 0;
if (timestamp >= BlockChain.getInstance().getQoraV2Timestamp())
txGroupId = byteBuffer.getInt();
byte[] reference = new byte[REFERENCE_LENGTH];
byteBuffer.get(reference);
byte[] creatorPublicKey = Serialization.deserializePublicKey(byteBuffer);
String target = Serialization.deserializeAddress(byteBuffer);
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
byte[] signature = new byte[SIGNATURE_LENGTH];
byteBuffer.get(signature);
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, creatorPublicKey, fee, signature);
return new EnableForgingTransactionData(baseTransactionData, target);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
return getBaseLength(transactionData) + EXTRAS_LENGTH;
}
public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
try {
EnableForgingTransactionData enableForgingTransactionData = (EnableForgingTransactionData) transactionData;
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
transformCommonBytes(transactionData, bytes);
Serialization.serializeAddress(bytes, enableForgingTransactionData.getTarget());
Serialization.serializeBigDecimal(bytes, enableForgingTransactionData.getFee());
if (enableForgingTransactionData.getSignature() != null)
bytes.write(enableForgingTransactionData.getSignature());
return bytes.toByteArray();
} catch (IOException | ClassCastException e) {
throw new TransformationException(e);
}
}
}