forked from Qortal/qortal
Rework distributing block reward to legacy qora holders
Fix related unit tests. Fix assetID support in GENESIS transactions.
This commit is contained in:
parent
62e2fd759c
commit
ebc2ee6ea9
@ -16,6 +16,8 @@ public class Asset {
|
||||
|
||||
/** Hard-coded asset representing legacy QORA held in old QORA1 blockchain. */
|
||||
public static final long LEGACY_QORA = 1L;
|
||||
/** Hard-coded asset representing QORT gained from holding legacy QORA. */
|
||||
public static final long QORT_FROM_QORA = 2L;
|
||||
|
||||
// Other useful constants
|
||||
|
||||
|
@ -8,7 +8,9 @@ import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
@ -28,6 +30,7 @@ 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.QortFromQoraData;
|
||||
import org.qora.data.account.RewardShareData;
|
||||
import org.qora.data.at.ATData;
|
||||
import org.qora.data.at.ATStateData;
|
||||
@ -37,6 +40,7 @@ import org.qora.data.block.BlockTransactionData;
|
||||
import org.qora.data.network.OnlineAccountData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.ATRepository;
|
||||
import org.qora.repository.AccountRepository.BalanceOrdering;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.TransactionRepository;
|
||||
@ -131,7 +135,6 @@ public class Block {
|
||||
final Account mintingAccount;
|
||||
final AccountData mintingAccountData;
|
||||
final boolean isMinterFounder;
|
||||
final BigDecimal minterQoraAmount;
|
||||
final int shareBin;
|
||||
|
||||
final Account recipientAccount;
|
||||
@ -146,12 +149,6 @@ public class Block {
|
||||
this.mintingAccount = new PublicKeyAccount(repository, this.rewardShareData.getMinterPublicKey());
|
||||
this.recipientAccount = new Account(repository, this.rewardShareData.getRecipient());
|
||||
|
||||
AccountBalanceData qoraBalanceData = repository.getAccountRepository().getBalance(this.mintingAccount.getAddress(), Asset.LEGACY_QORA);
|
||||
if (qoraBalanceData != null && qoraBalanceData.getBalance() != null && qoraBalanceData.getBalance().compareTo(BigDecimal.ZERO) > 0)
|
||||
this.minterQoraAmount = qoraBalanceData.getBalance();
|
||||
else
|
||||
this.minterQoraAmount = null;
|
||||
|
||||
this.mintingAccountData = repository.getAccountRepository().getAccount(this.mintingAccount.getAddress());
|
||||
this.isMinterFounder = Account.isFounder(mintingAccountData.getFlags());
|
||||
|
||||
@ -1261,7 +1258,7 @@ public class Block {
|
||||
if (reward == null)
|
||||
return;
|
||||
|
||||
distributeByAccountLevel(reward);
|
||||
distributeBlockReward(reward);
|
||||
}
|
||||
|
||||
protected void processTransactions() throws DataException {
|
||||
@ -1344,7 +1341,7 @@ public class Block {
|
||||
if (blockFees.compareTo(BigDecimal.ZERO) <= 0)
|
||||
return;
|
||||
|
||||
distributeByAccountLevel(blockFees);
|
||||
distributeBlockReward(blockFees);
|
||||
}
|
||||
|
||||
protected void processAtFeesAndStates() throws DataException {
|
||||
@ -1486,7 +1483,7 @@ public class Block {
|
||||
if (reward == null)
|
||||
return;
|
||||
|
||||
distributeByAccountLevel(reward.negate());
|
||||
distributeBlockReward(reward.negate());
|
||||
}
|
||||
|
||||
protected void deductTransactionFees() throws DataException {
|
||||
@ -1496,7 +1493,7 @@ public class Block {
|
||||
if (blockFees.compareTo(BigDecimal.ZERO) <= 0)
|
||||
return;
|
||||
|
||||
distributeByAccountLevel(blockFees.negate());
|
||||
distributeBlockReward(blockFees.negate());
|
||||
}
|
||||
|
||||
protected void orphanAtFeesAndStates() throws DataException {
|
||||
@ -1562,7 +1559,9 @@ public class Block {
|
||||
}
|
||||
}
|
||||
|
||||
protected void distributeByAccountLevel(BigDecimal totalAmount) throws DataException {
|
||||
protected void distributeBlockReward(BigDecimal totalAmount) throws DataException {
|
||||
LOGGER.trace(() -> String.format("Distributing: %s", totalAmount.toPlainString()));
|
||||
|
||||
List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
|
||||
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||
|
||||
@ -1593,33 +1592,111 @@ 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<ExpandedAccount> qoraHolderAccounts = new ArrayList<>();
|
||||
BigDecimal totalQoraHeld = BigDecimal.ZERO;
|
||||
for (int i = 0; i < expandedAccounts.size(); ++i) {
|
||||
ExpandedAccount expandedAccount = expandedAccounts.get(i);
|
||||
if (expandedAccount.minterQoraAmount == null)
|
||||
continue;
|
||||
List<String> assetAddresses = Collections.emptyList();
|
||||
List<Long> assetIds = Collections.singletonList(Asset.LEGACY_QORA);
|
||||
List<AccountBalanceData> qoraHolders = this.repository.getAccountRepository().getAssetBalances(assetAddresses, assetIds, BalanceOrdering.ASSET_ACCOUNT, true, null, null, null);
|
||||
|
||||
qoraHolderAccounts.add(expandedAccount);
|
||||
totalQoraHeld = totalQoraHeld.add(expandedAccount.minterQoraAmount);
|
||||
// Filter out qoraHolders who have received max QORT due to holding legacy QORA, (ratio from blockchain config)
|
||||
BigDecimal qoraPerQortReward = BlockChain.getInstance().getQoraPerQortReward();
|
||||
Iterator<AccountBalanceData> qoraHoldersIterator = qoraHolders.iterator();
|
||||
while (qoraHoldersIterator.hasNext()) {
|
||||
AccountBalanceData qoraHolder = qoraHoldersIterator.next();
|
||||
|
||||
Account qoraHolderAccount = new Account(repository, qoraHolder.getAddress());
|
||||
BigDecimal qortFromQora = qoraHolderAccount.getConfirmedBalance(Asset.QORT_FROM_QORA);
|
||||
|
||||
// If we're processing a block, then totalAmount will be positive
|
||||
if (totalAmount.signum() >= 0) {
|
||||
BigDecimal maxQortFromQora = qoraHolder.getBalance().divide(qoraPerQortReward, RoundingMode.DOWN);
|
||||
|
||||
// Disregard qora holders who have already received maximum qort from holding legacy qora
|
||||
if (qortFromQora.compareTo(maxQortFromQora) >= 0)
|
||||
qoraHoldersIterator.remove();
|
||||
} else {
|
||||
// We're orphaning a block
|
||||
// so disregard qora holders whose final block is earlier than this one
|
||||
QortFromQoraData qortFromQoraData = this.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
|
||||
if (qortFromQoraData == null)
|
||||
throw new IllegalStateException(String.format("Missing QORT-from-QORA data for %s", qoraHolder.getAddress()));
|
||||
|
||||
if (qortFromQoraData.getFinalBlockHeight() != null && qortFromQoraData.getFinalBlockHeight() < this.blockData.getHeight())
|
||||
qoraHoldersIterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
final BigDecimal finalTotalQoraHeld = totalQoraHeld;
|
||||
BigDecimal totalQoraHeld = BigDecimal.ZERO;
|
||||
for (int i = 0; i < qoraHolders.size(); ++i)
|
||||
totalQoraHeld = totalQoraHeld.add(qoraHolders.get(i).getBalance());
|
||||
|
||||
BigDecimal finalTotalQoraHeld = totalQoraHeld;
|
||||
LOGGER.trace(() -> String.format("Total legacy QORA held: %s", finalTotalQoraHeld.toPlainString()));
|
||||
|
||||
for (int h = 0; h < qoraHolderAccounts.size(); ++h) {
|
||||
ExpandedAccount expandedAccount = qoraHolderAccounts.get(h);
|
||||
final BigDecimal holderAmount = qoraHoldersAmount.multiply(totalQoraHeld).divide(expandedAccount.minterQoraAmount, RoundingMode.DOWN);
|
||||
LOGGER.trace(() -> String.format("Minter account %s has %s / %s QORA so share: %s",
|
||||
expandedAccount.mintingAccount.getAddress(), expandedAccount.minterQoraAmount, finalTotalQoraHeld, holderAmount.toPlainString()));
|
||||
for (int h = 0; h < qoraHolders.size(); ++h) {
|
||||
AccountBalanceData qoraHolder = qoraHolders.get(h);
|
||||
|
||||
expandedAccount.distribute(holderAmount);
|
||||
sharedAmount = sharedAmount.add(holderAmount);
|
||||
final BigDecimal holderReward = qoraHoldersAmount.multiply(totalQoraHeld).divide(qoraHolder.getBalance(), RoundingMode.DOWN).setScale(8, RoundingMode.DOWN);
|
||||
LOGGER.trace(() -> String.format("QORA holder %s has %s / %s QORA so share: %s",
|
||||
qoraHolder.getAddress(), qoraHolder.getBalance().toPlainString(), finalTotalQoraHeld, holderReward.toPlainString()));
|
||||
|
||||
Account qoraHolderAccount = new Account(repository, qoraHolder.getAddress());
|
||||
QortFromQoraData qortFromQoraData = this.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
|
||||
if (qortFromQoraData == null)
|
||||
qortFromQoraData = new QortFromQoraData(qoraHolder.getAddress(), BigDecimal.ZERO.setScale(8), null);
|
||||
|
||||
BigDecimal qortFromQora = holderReward.divide(qoraPerQortReward, RoundingMode.DOWN);
|
||||
|
||||
BigDecimal newQortFromQoraBalance = qoraHolderAccount.getConfirmedBalance(Asset.QORT_FROM_QORA).add(qortFromQora);
|
||||
|
||||
// If processing, make sure we don't overpay
|
||||
if (totalAmount.signum() >= 0) {
|
||||
BigDecimal maxQortFromQora = qoraHolder.getBalance().divide(qoraPerQortReward, RoundingMode.DOWN);
|
||||
|
||||
if (newQortFromQoraBalance.compareTo(maxQortFromQora) >= 0) {
|
||||
// Reduce final QORT-from-QORA payment to match max
|
||||
BigDecimal adjustment = newQortFromQoraBalance.subtract(maxQortFromQora);
|
||||
|
||||
qortFromQora = qortFromQora.subtract(adjustment);
|
||||
newQortFromQoraBalance = newQortFromQoraBalance.subtract(adjustment);
|
||||
|
||||
// This is also qora holders final qort-from-qora block
|
||||
qortFromQoraData.setFinalQortFromQora(qortFromQora);
|
||||
qortFromQoraData.setFinalBlockHeight(this.blockData.getHeight());
|
||||
|
||||
BigDecimal finalQortFromQora = qortFromQora;
|
||||
LOGGER.trace(() -> String.format("QORA holder %s final share %s at height %d",
|
||||
qoraHolder.getAddress(), finalQortFromQora.toPlainString(), this.blockData.getHeight()));
|
||||
}
|
||||
} else {
|
||||
// Orphaning
|
||||
if (qortFromQoraData.getFinalBlockHeight() != null) {
|
||||
// Note use of negate() here as qortFromQora will be negative during orphaning,
|
||||
// but final qort-from-qora is stored in repository during processing (and hence positive).
|
||||
BigDecimal adjustment = qortFromQora.subtract(qortFromQoraData.getFinalQortFromQora().negate());
|
||||
|
||||
qortFromQora = qortFromQora.subtract(adjustment);
|
||||
newQortFromQoraBalance = newQortFromQoraBalance.subtract(adjustment);
|
||||
|
||||
qortFromQoraData.setFinalQortFromQora(null);
|
||||
qortFromQoraData.setFinalBlockHeight(null);
|
||||
|
||||
BigDecimal finalQortFromQora = qortFromQora;
|
||||
LOGGER.trace(() -> String.format("QORA holder %s final share %s was at height %d",
|
||||
qoraHolder.getAddress(), finalQortFromQora.toPlainString(), this.blockData.getHeight()));
|
||||
}
|
||||
}
|
||||
|
||||
qoraHolderAccount.setConfirmedBalance(Asset.QORT, qoraHolderAccount.getConfirmedBalance(Asset.QORT).add(qortFromQora));
|
||||
qoraHolderAccount.setConfirmedBalance(Asset.QORT_FROM_QORA, newQortFromQoraBalance);
|
||||
|
||||
this.repository.getAccountRepository().save(qortFromQoraData);
|
||||
|
||||
sharedAmount = sharedAmount.add(holderReward);
|
||||
}
|
||||
|
||||
// Spread remainder across founder accounts
|
||||
BigDecimal foundersAmount = totalAmount.subtract(sharedAmount);
|
||||
LOGGER.debug(String.format("Shared %s of %s, remaining %s to founders", sharedAmount.toPlainString(), totalAmount.toPlainString(), foundersAmount.toPlainString()));
|
||||
BigDecimal finalSharedAmount = sharedAmount;
|
||||
LOGGER.debug(() -> String.format("Shared %s of %s, remaining %s to founders", finalSharedAmount.toPlainString(), totalAmount.toPlainString(), foundersAmount.toPlainString()));
|
||||
|
||||
List<ExpandedAccount> founderAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.isMinterFounder).collect(Collectors.toList());
|
||||
if (founderAccounts.isEmpty())
|
||||
|
@ -109,6 +109,8 @@ public class BlockChain {
|
||||
|
||||
/** Share of block reward/fees to legacy QORA coin holders */
|
||||
BigDecimal qoraHoldersShare;
|
||||
/** How many legacy QORA per 1 QORT of block reward. */
|
||||
BigDecimal qoraPerQortReward;
|
||||
|
||||
/**
|
||||
* Number of minted blocks required to reach next level from previous.
|
||||
@ -312,6 +314,10 @@ public class BlockChain {
|
||||
return this.qoraHoldersShare;
|
||||
}
|
||||
|
||||
public BigDecimal getQoraPerQortReward() {
|
||||
return this.qoraPerQortReward;
|
||||
}
|
||||
|
||||
public int getMinAccountLevelToMint() {
|
||||
return this.minAccountLevelToMint;
|
||||
}
|
||||
@ -403,6 +409,9 @@ public class BlockChain {
|
||||
if (this.qoraHoldersShare == null)
|
||||
Settings.throwValidationError("No \"qoraHoldersShare\" entry found in blockchain config");
|
||||
|
||||
if (this.qoraPerQortReward == null)
|
||||
Settings.throwValidationError("No \"qoraPerQortReward\" entry found in blockchain config");
|
||||
|
||||
if (this.blocksNeededByLevel == null)
|
||||
Settings.throwValidationError("No \"blocksNeededByLevel\" entry found in blockchain config");
|
||||
|
||||
|
@ -360,6 +360,8 @@ public class BlockMinter extends Thread {
|
||||
|
||||
// Add to blockchain
|
||||
newBlock.process();
|
||||
LOGGER.info(String.format("Minted new test block: %d", newBlock.getBlockData().getHeight()));
|
||||
|
||||
repository.saveChanges();
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
|
52
src/main/java/org/qora/data/account/QortFromQoraData.java
Normal file
52
src/main/java/org/qora/data/account/QortFromQoraData.java
Normal file
@ -0,0 +1,52 @@
|
||||
package org.qora.data.account;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class QortFromQoraData {
|
||||
|
||||
// Properties
|
||||
private String address;
|
||||
// Not always present:
|
||||
private BigDecimal finalQortFromQora;
|
||||
private Integer finalBlockHeight;
|
||||
|
||||
// Constructors
|
||||
|
||||
// necessary for JAXB
|
||||
protected QortFromQoraData() {
|
||||
}
|
||||
|
||||
public QortFromQoraData(String address, BigDecimal finalQortFromQora, Integer finalBlockHeight) {
|
||||
this.address = address;
|
||||
this.finalQortFromQora = finalQortFromQora;
|
||||
this.finalBlockHeight = finalBlockHeight;
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public BigDecimal getFinalQortFromQora() {
|
||||
return this.finalQortFromQora;
|
||||
}
|
||||
|
||||
public void setFinalQortFromQora(BigDecimal finalQortFromQora) {
|
||||
this.finalQortFromQora = finalQortFromQora;
|
||||
}
|
||||
|
||||
public Integer getFinalBlockHeight() {
|
||||
return this.finalBlockHeight;
|
||||
}
|
||||
|
||||
public void setFinalBlockHeight(Integer finalBlockHeight) {
|
||||
this.finalBlockHeight = finalBlockHeight;
|
||||
}
|
||||
|
||||
}
|
@ -5,6 +5,7 @@ import java.util.List;
|
||||
import org.qora.data.account.AccountBalanceData;
|
||||
import org.qora.data.account.AccountData;
|
||||
import org.qora.data.account.MintingAccountData;
|
||||
import org.qora.data.account.QortFromQoraData;
|
||||
import org.qora.data.account.RewardShareData;
|
||||
|
||||
public interface AccountRepository {
|
||||
@ -138,4 +139,10 @@ public interface AccountRepository {
|
||||
/** Delete minting account info, used by BlockMinter, from repository using passed private key. */
|
||||
public int delete(byte[] mintingAccountPrivateKey) throws DataException;
|
||||
|
||||
// Managing QORT from legacy QORA
|
||||
|
||||
public QortFromQoraData getQortFromQoraInfo(String address) throws DataException;
|
||||
|
||||
public void save(QortFromQoraData qortFromQoraData) throws DataException;
|
||||
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import java.util.List;
|
||||
import org.qora.data.account.AccountBalanceData;
|
||||
import org.qora.data.account.AccountData;
|
||||
import org.qora.data.account.MintingAccountData;
|
||||
import org.qora.data.account.QortFromQoraData;
|
||||
import org.qora.data.account.RewardShareData;
|
||||
import org.qora.repository.AccountRepository;
|
||||
import org.qora.repository.DataException;
|
||||
@ -660,4 +661,38 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// Managing QORT from legacy QORA
|
||||
|
||||
public QortFromQoraData getQortFromQoraInfo(String address) throws DataException {
|
||||
String sql = "SELECT final_qort_from_qora, final_block_height FROM AccountQortFromQoraInfo WHERE account = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
BigDecimal finalQortFromQora = resultSet.getBigDecimal(1);
|
||||
Integer finalBlockHeight = resultSet.getInt(2);
|
||||
if (finalBlockHeight == 0 && resultSet.wasNull())
|
||||
finalBlockHeight = null;
|
||||
|
||||
return new QortFromQoraData(address, finalQortFromQora, finalBlockHeight);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch account qort-from-qora info from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void save(QortFromQoraData qortFromQoraData) throws DataException {
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountQortFromQoraInfo");
|
||||
|
||||
saveHelper.bind("account", qortFromQoraData.getAddress())
|
||||
.bind("final_qort_from_qora", qortFromQoraData.getFinalQortFromQora())
|
||||
.bind("final_block_height", qortFromQoraData.getFinalBlockHeight());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to save account qort-from-qora info into repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -841,6 +841,12 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("DROP INDEX BlockGenerationHeightIndex");
|
||||
break;
|
||||
|
||||
case 59:
|
||||
// Keeping track of QORT gained from holding legacy QORA
|
||||
stmt.execute("CREATE TABLE AccountQortFromQoraInfo (account QoraAddress, final_qort_from_qora QoraAmount, final_block_height INT, "
|
||||
+ "PRIMARY KEY (account), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)");
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@ -7,7 +7,6 @@ import java.util.List;
|
||||
|
||||
import org.qora.account.Account;
|
||||
import org.qora.account.PrivateKeyAccount;
|
||||
import org.qora.asset.Asset;
|
||||
import org.qora.crypto.Crypto;
|
||||
import org.qora.data.transaction.GenesisTransactionData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
@ -139,7 +138,7 @@ public class GenesisTransaction extends Transaction {
|
||||
Account recipient = new Account(repository, genesisTransactionData.getRecipient());
|
||||
|
||||
// Update recipient's balance
|
||||
recipient.setConfirmedBalance(Asset.QORT, genesisTransactionData.getAmount());
|
||||
recipient.setConfirmedBalance(genesisTransactionData.getAssetId(), genesisTransactionData.getAmount());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -2,6 +2,8 @@ package org.qora.test.common;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qora.account.PrivateKeyAccount;
|
||||
import org.qora.block.Block;
|
||||
import org.qora.block.BlockChain;
|
||||
@ -12,6 +14,8 @@ import org.qora.repository.Repository;
|
||||
|
||||
public class BlockUtils {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(BlockUtils.class);
|
||||
|
||||
/** Mints a new block using "alice-reward-share" test account. */
|
||||
public static void mintBlock(Repository repository) throws DataException {
|
||||
PrivateKeyAccount mintingAccount = Common.getTestAccount(repository, "alice-reward-share");
|
||||
@ -26,8 +30,14 @@ public class BlockUtils {
|
||||
|
||||
public static void orphanLastBlock(Repository repository) throws DataException {
|
||||
BlockData blockData = repository.getBlockRepository().getLastBlock();
|
||||
|
||||
final int height = blockData.getHeight();
|
||||
|
||||
Block block = new Block(repository, blockData);
|
||||
block.orphan();
|
||||
|
||||
LOGGER.info(String.format("Orphaned block: %d", height));
|
||||
|
||||
repository.saveChanges();
|
||||
}
|
||||
|
||||
|
@ -16,16 +16,13 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bitcoinj.core.Base58;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.qora.block.Block;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.data.account.AccountBalanceData;
|
||||
import org.qora.data.asset.AssetData;
|
||||
import org.qora.data.block.BlockData;
|
||||
import org.qora.data.group.GroupData;
|
||||
import org.qora.repository.AccountRepository.BalanceOrdering;
|
||||
import org.qora.repository.DataException;
|
||||
@ -65,11 +62,6 @@ public class Common {
|
||||
private static List<GroupData> initialGroups;
|
||||
private static List<AccountBalanceData> initialBalances;
|
||||
|
||||
// TODO: converts users of these constants to TestAccount schema
|
||||
public static final byte[] v2testPrivateKey = Base58.decode("A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6");
|
||||
public static final byte[] v2testPublicKey = Base58.decode("2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP");
|
||||
public static final String v2testAddress = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v";
|
||||
|
||||
private static Map<String, TestAccount> testAccountsByName = new HashMap<>();
|
||||
static {
|
||||
testAccountsByName.put("alice", new TestAccount(null, "alice", "A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6", false));
|
||||
@ -131,10 +123,7 @@ public class Common {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Orphan back to genesis block
|
||||
while (repository.getBlockRepository().getBlockchainHeight() > 1) {
|
||||
BlockData blockData = repository.getBlockRepository().getLastBlock();
|
||||
Block block = new Block(repository, blockData);
|
||||
block.orphan();
|
||||
repository.saveChanges();
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
}
|
||||
|
||||
List<AssetData> remainingAssets = repository.getAssetRepository().getAllAssets();
|
||||
@ -172,6 +161,9 @@ public class Common {
|
||||
List<T> remainingClone = new ArrayList<T>(remaining);
|
||||
remainingClone.removeIf(isInitial);
|
||||
|
||||
for (T remainingEntry : remainingClone)
|
||||
LOGGER.info(String.format("Non-genesis remaining entry: %s", keyExtractor.apply(remainingEntry)));
|
||||
|
||||
assertTrue(String.format("Non-genesis %s remains", typeName), remainingClone.isEmpty());
|
||||
}
|
||||
|
||||
|
@ -48,23 +48,24 @@ public class RewardTests extends Common {
|
||||
|
||||
@Test
|
||||
public void testRewards() throws DataException {
|
||||
List<RewardByHeight> rewardsByHeight = BlockChain.getInstance().getBlockRewardsByHeight();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT);
|
||||
|
||||
List<RewardByHeight> rewards = BlockChain.getInstance().getBlockRewardsByHeight();
|
||||
int rewardIndex = rewardsByHeight.size() - 1;
|
||||
|
||||
int rewardIndex = rewards.size() - 1;
|
||||
|
||||
RewardByHeight rewardInfo = rewards.get(rewardIndex);
|
||||
RewardByHeight rewardInfo = rewardsByHeight.get(rewardIndex);
|
||||
BigDecimal expectedBalance = initialBalances.get("alice").get(Asset.QORT);
|
||||
|
||||
for (int height = rewardInfo.height; height > 1; --height) {
|
||||
if (height < rewardInfo.height) {
|
||||
--rewardIndex;
|
||||
rewardInfo = rewards.get(rewardIndex);
|
||||
rewardInfo = rewardsByHeight.get(rewardIndex);
|
||||
}
|
||||
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
expectedBalance = expectedBalance.add(rewardInfo.reward);
|
||||
}
|
||||
|
||||
@ -82,6 +83,7 @@ public class RewardTests extends Common {
|
||||
|
||||
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT);
|
||||
BigDecimal blockReward = BlockUtils.getNextBlockReward(repository);
|
||||
|
||||
BlockMinter.mintTestingBlock(repository, rewardShareAccount);
|
||||
|
||||
// We're expecting reward * 12.8% to Bob, the rest to Alice
|
||||
@ -94,4 +96,47 @@ public class RewardTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testLegacyQoraReward() throws DataException {
|
||||
Common.useSettings("test-settings-v2-qora-holder.json");
|
||||
|
||||
BigDecimal qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare();
|
||||
BigDecimal qoraPerQort = BlockChain.getInstance().getQoraPerQortReward();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.QORT_FROM_QORA);
|
||||
|
||||
BigDecimal blockReward = BlockUtils.getNextBlockReward(repository);
|
||||
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Expected reward
|
||||
BigDecimal expectedReward = blockReward.multiply(qoraHoldersShare).divide(qoraPerQort, RoundingMode.DOWN);
|
||||
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, initialBalances.get("chloe").get(Asset.QORT).add(expectedReward));
|
||||
|
||||
AccountUtils.assertBalance(repository, "chloe", Asset.QORT_FROM_QORA, initialBalances.get("chloe").get(Asset.QORT_FROM_QORA).add(expectedReward));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMaxLegacyQoraReward() throws DataException {
|
||||
Common.useSettings("test-settings-v2-qora-holder.json");
|
||||
|
||||
BigDecimal qoraPerQort = BlockChain.getInstance().getQoraPerQortReward();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA);
|
||||
|
||||
// Mint lots of blocks
|
||||
for (int i = 0; i < 100; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Expected balances to be limited by Chloe's legacy QORA amount
|
||||
BigDecimal expectedBalance = initialBalances.get("chloe").get(Asset.LEGACY_QORA).divide(qoraPerQort);
|
||||
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, initialBalances.get("chloe").get(Asset.QORT).add(expectedBalance));
|
||||
AccountUtils.assertBalance(repository, "chloe", Asset.QORT_FROM_QORA, initialBalances.get("chloe").get(Asset.QORT_FROM_QORA).add(expectedBalance));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
69
src/test/resources/test-chain-v2-qora-holder.json
Normal file
69
src/test/resources/test-chain-v2-qora-holder.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"isTestChain": true,
|
||||
"blockTimestampMargin": 500,
|
||||
"transactionExpiryPeriod": 86400000,
|
||||
"maxBlockSize": 2097152,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.1",
|
||||
"requireGroupForApproval": false,
|
||||
"minAccountLevelToRewardShare": 5,
|
||||
"maxRewardSharesPerMintingAccount": 20,
|
||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
{ "height": 21, "reward": 1 }
|
||||
],
|
||||
"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,
|
||||
"qoraPerQortReward": 250,
|
||||
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
|
||||
"blockTimingsByHeight": [
|
||||
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
||||
],
|
||||
"featureTriggers": {
|
||||
"messageHeight": 0,
|
||||
"atHeight": 0,
|
||||
"assetsTimestamp": 0,
|
||||
"votingTimestamp": 0,
|
||||
"arbitraryTimestamp": 0,
|
||||
"powfixTimestamp": 0,
|
||||
"v2Timestamp": 0,
|
||||
"newAssetPricingTimestamp": 0,
|
||||
"groupApprovalTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
"timestamp": 0,
|
||||
"transactions": [
|
||||
{ "type": "ISSUE_ASSET", "owner": "QcFmNxSArv5tWEzCtTKb2Lqc5QkKuQ7RNs", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||
|
||||
{ "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" },
|
||||
{ "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" },
|
||||
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" },
|
||||
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" },
|
||||
|
||||
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "100", "assetId": 1 },
|
||||
|
||||
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
|
||||
|
||||
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "TEST", "description": "test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
|
||||
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 },
|
||||
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 8 }
|
||||
]
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@
|
||||
{ "levels": [ 9, 10 ], "share": 0.25 }
|
||||
],
|
||||
"qoraHoldersShare": 0.20,
|
||||
"qoraPerQortReward": 250,
|
||||
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
|
||||
"blockTimingsByHeight": [
|
||||
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
||||
@ -42,18 +43,25 @@
|
||||
"version": 4,
|
||||
"timestamp": 0,
|
||||
"transactions": [
|
||||
{ "type": "ISSUE_ASSET", "owner": "QcFmNxSArv5tWEzCtTKb2Lqc5QkKuQ7RNs", "assetName": "QORA", "description": "QORA native coin", "data": "", "quantity": 10000000000, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
|
||||
{ "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000", "fee": 0 },
|
||||
{ "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000", "fee": 0 },
|
||||
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000", "fee": 0 },
|
||||
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000", "fee": 0 },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QcFmNxSArv5tWEzCtTKb2Lqc5QkKuQ7RNs", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||
|
||||
{ "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" },
|
||||
{ "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" },
|
||||
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" },
|
||||
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" },
|
||||
|
||||
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
|
||||
|
||||
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "TEST", "description": "test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
|
||||
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 8 },
|
||||
{ "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 }
|
||||
{ "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 },
|
||||
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 8 }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
6
src/test/resources/test-settings-v2-qora-holder.json
Normal file
6
src/test/resources/test-settings-v2-qora-holder.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2-qora-holder.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"minPeers": 0
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user