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);
}
}
}

View File

@ -1,13 +1,9 @@
{
"maxBalance": "10000000",
"blockDifficultyInterval": 10,
"minBlockTime": 60,
"maxBlockTime": 300,
"blockTimestampMargin": 2000,
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 1048576,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "1.0",
"unitFee": "0.001",
"useBrokenMD160ForAddresses": false,
"requireGroupForApproval": false,
"defaultGroupId": 0,
@ -18,9 +14,9 @@
"genesisInfo": {
"version": 4,
"timestamp": "1569510000000",
"generatingBalance": "100000",
"transactions": [
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT", "description": "QORTAL coin", "quantity": 10000000, "isDivisible": true, "fee": 0, "reference": "28u54WRcMfGujtQMZ9dNKFXVqucY7XfPihXAqPFsnx853NPUwfDJy1sMH5boCkahFgjUNYqc5fkduxdBhQTKgUsC", "data": "{}" },
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT", "description": "QORTAL coin", "quantity": 10000000, "isDivisible": true, "reference": "28u54WRcMfGujtQMZ9dNKFXVqucY7XfPihXAqPFsnx853NPUwfDJy1sMH5boCkahFgjUNYqc5fkduxdBhQTKgUsC", "data": "{}" },
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORA", "description": "Representative legacy QORA", "quantity": 10000000000, "isDivisible": true, "data": "{}", "isUnspendable": true },
{ "type": "GENESIS", "recipient": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "amount": "1000000" },
{ "type": "GENESIS", "recipient": "QcatoCyyp7dVfMtJ92sgUUPDoBJevaemRX", "amount": "1000000" },
{ "type": "GENESIS", "recipient": "QTiga19sttbf6CLQLT83mhCSWEaCvjk8th", "amount": "1000000" },
@ -39,17 +35,26 @@
]
},
"rewardsByHeight": [
{ "height": 1, "reward": 0 },
{ "height": 100, "reward": 100 },
{ "height": 200, "reward": 20 },
{ "height": 1000, "reward": 1 },
{ "height": 2000, "reward": 0 }
{ "height": 2, "reward": 5.0000 },
{ "height": 259204, "reward": 4.7500 },
{ "height": 518406, "reward": 4.5125 },
{ "height": 777608, "reward": 4.2869 },
{ "height": 1036810, "reward": 4.0725 },
{ "height": 1296012, "reward": 3.8689 },
{ "height": 1555214, "reward": 3.6755 },
{ "height": 1814416, "reward": 3.4917 },
{ "height": 2073618, "reward": 3.3171 },
{ "height": 2332820, "reward": 3.1512 }
],
"forgingTiers": [
{ "minBlocks": 50, "maxSubAccounts": 5 },
{ "minBlocks": 50, "maxSubAccounts": 1 },
{ "minBlocks": 0, "maxSubAccounts": 0 }
"sharesByLevel": [
{ "levels": [ 1, 2 ], "share": 0.05 },
{ "levels": [ 3, 4 ], "share": 0.10 },
{ "levels": [ 5, 6 ], "share": 0.15 },
{ "levels": [ 7, 8 ], "share": 0.20 },
{ "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"blocksNeededByLevel": [ 7200, 64800, 129600, 172800, 244000, 345600, 518400, 691200, 864000, 1036800 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
],

View File

@ -105,7 +105,7 @@ public class TransactionTests extends Common {
// Create test generator account
generator = new PrivateKeyAccount(repository, generatorSeed);
accountRepository.setLastReference(new AccountData(generator.getAddress(), generatorSeed, generator.getPublicKey(), Group.NO_GROUP, 0, null, 0, 0));
accountRepository.setLastReference(new AccountData(generator.getAddress(), generatorSeed, generator.getPublicKey(), Group.NO_GROUP, 0, 0, 0, 0));
accountRepository.save(new AccountBalanceData(generator.getAddress(), Asset.QORT, initialGeneratorBalance));
// Create test sender account
@ -113,7 +113,7 @@ public class TransactionTests extends Common {
// Mock account
reference = senderSeed;
accountRepository.setLastReference(new AccountData(sender.getAddress(), reference, sender.getPublicKey(), Group.NO_GROUP, 0, null, 0, 0));
accountRepository.setLastReference(new AccountData(sender.getAddress(), reference, sender.getPublicKey(), Group.NO_GROUP, 0, 0, 0, 0));
// Mock balance
accountRepository.save(new AccountBalanceData(sender.getAddress(), Asset.QORT, initialSenderBalance));

View File

@ -1,162 +0,0 @@
package org.qora.test.forging;
import static org.junit.Assert.*;
import java.util.Random;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qora.account.PrivateKeyAccount;
import org.qora.block.BlockChain;
import org.qora.block.BlockGenerator;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.test.common.AccountUtils;
import org.qora.test.common.Common;
import org.qora.test.common.TransactionUtils;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.ValidationResult;
public class GrantForgingTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@After
public void afterTest() throws DataException {
Common.orphanCheck();
}
@Test
public void testSimpleGrant() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount forgingAccount = Common.getTestAccount(repository, "alice");
TransactionData transactionData = AccountUtils.createEnableForging(repository, "alice", "bob");
Transaction transaction = Transaction.fromData(repository, transactionData);
transaction.sign(forgingAccount);
ValidationResult result = transaction.isValidUnconfirmed();
// Alice can't grant without forging minimum number of blocks
assertEquals(ValidationResult.FORGE_MORE_BLOCKS, result);
// Forge a load of blocks
int blocksNeeded = BlockChain.getInstance().getForgingTiers().get(0).minBlocks;
for (int i = 0; i < blocksNeeded; ++i)
BlockGenerator.generateTestingBlock(repository, forgingAccount);
// Alice should be able to grant now
result = transaction.isValidUnconfirmed();
assertEquals(ValidationResult.OK, result);
TransactionUtils.signAndForge(repository, transactionData, forgingAccount);
}
}
@Test
public void testMaxGrant() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount forgingAccount = Common.getTestAccount(repository, "alice");
TransactionData transactionData = AccountUtils.createEnableForging(repository, "alice", "bob");
Transaction transaction = Transaction.fromData(repository, transactionData);
transaction.sign(forgingAccount);
ValidationResult result = transaction.isValidUnconfirmed();
// Alice can't grant without forging minimum number of blocks
assertEquals(ValidationResult.FORGE_MORE_BLOCKS, result);
// Forge a load of blocks
int blocksNeeded = BlockChain.getInstance().getForgingTiers().get(0).minBlocks;
for (int i = 0; i < blocksNeeded; ++i)
BlockGenerator.generateTestingBlock(repository, forgingAccount);
// Alice should be able to grant up to 5 now
// Gift to random accounts
Random random = new Random();
for (int i = 0; i < 5; ++i) {
byte[] publicKey = new byte[32];
random.nextBytes(publicKey);
transactionData = AccountUtils.createEnableForging(repository, "alice", publicKey);
transaction = Transaction.fromData(repository, transactionData);
transaction.sign(forgingAccount);
result = transaction.isValidUnconfirmed();
assertEquals("Couldn't enable account #" + i, ValidationResult.OK, result);
TransactionUtils.signAndForge(repository, transactionData, forgingAccount);
}
// Alice's allocation used up
byte[] publicKey = new byte[32];
random.nextBytes(publicKey);
transactionData = AccountUtils.createEnableForging(repository, "alice", publicKey);
transaction = Transaction.fromData(repository, transactionData);
transaction.sign(forgingAccount);
result = transaction.isValidUnconfirmed();
assertEquals(ValidationResult.FORGING_ENABLE_LIMIT, result);
}
}
@Test
public void testFinalTier() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount aliceAccount = Common.getTestAccount(repository, "alice");
TransactionData transactionData = AccountUtils.createEnableForging(repository, "alice", "bob");
Transaction transaction = Transaction.fromData(repository, transactionData);
transaction.sign(aliceAccount);
ValidationResult result = transaction.isValidUnconfirmed();
// Alice can't grant without forging minimum number of blocks
assertEquals(ValidationResult.FORGE_MORE_BLOCKS, result);
// Forge a load of blocks
int blocksNeeded = BlockChain.getInstance().getForgingTiers().get(0).minBlocks;
for (int i = 0; i < blocksNeeded; ++i)
BlockGenerator.generateTestingBlock(repository, aliceAccount);
// Alice should be able to grant now
AccountUtils.enableForging(repository, "alice", "bob");
// Bob can't grant without forging minimum number of blocks
transactionData = AccountUtils.createEnableForging(repository, "bob", "chloe");
transaction = Transaction.fromData(repository, transactionData);
transaction.sign(aliceAccount);
result = transaction.isValidUnconfirmed();
assertEquals(ValidationResult.FORGE_MORE_BLOCKS, result);
// Bob needs to forge a load of blocks
PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
blocksNeeded = BlockChain.getInstance().getForgingTiers().get(1).minBlocks;
for (int i = 0; i < blocksNeeded; ++i)
BlockGenerator.generateTestingBlock(repository, bobAccount);
// Bob should be able to grant now
AccountUtils.enableForging(repository, "bob", "chloe");
// Chloe is final tier so shouldn't be able to grant
Random random = new Random();
byte[] publicKey = new byte[32];
random.nextBytes(publicKey);
transactionData = AccountUtils.createEnableForging(repository, "chloe", publicKey);
transaction = Transaction.fromData(repository, transactionData);
transaction.sign(aliceAccount);
result = transaction.isValidUnconfirmed();
assertEquals(ValidationResult.FORGING_ENABLE_LIMIT, result);
}
}
}