mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-23 04:36:50 +00:00
Rework distributing block reward to legacy qora holders
Fix related unit tests. Fix assetID support in GENESIS transactions.
This commit is contained in:
@@ -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
|
||||
|
Reference in New Issue
Block a user