forked from Qortal/qortal
Interim work on TRANSFER_PRIVS transaction
Converted AccountData's initialLevel to blocksMintedAdjustment. Corresponding changes to AccountLevelTransaction so that level set in genesis block is converted to blocksMintedAdjustment, via cumulativeBlocksByLevel. Ditto changes to HSQLDBAccountRepository, HSQLDBDatabaseUpdates, [HSQLDB]TransactionRepository, etc. Changes to API call POST /admin/mintingaccounts to check passed reward-share private key maps to a reward-share with minting account that still has privilege to mint. It's possible for a TRANSFER_PRIVS transaction to transfer away minting privileges from a minting account referenced by in a reward-share. Change to RewardShareTransaction to allow users to cancel a reward-share even if minting-account component no longer has minting privs. This should allow users to clean up more after a privs transfer. Re-order processing/orphaning in Block.process()/Block.orphan() to be more consistent and also to take in account changes that might have been caused by TRANSFER_PRIVS transactions which affect who might actually receive block rewards/tx fees. Founders now gain blocksMinted & levels as part of minting blocks. (Needed to make TRANSFER_PRIVS from a founder account to work). BlockMinter now has added checks to make sure that the reward-shares it might use to mint blocks still have valid minting-accounts. i.e. that the minting-account component of reward-share hasn't had minting privs transferred away by TRANSFER_PRIVS tx. Controller now rejects online-accounts from peers that no longer have minting privs (e.g. transferred away by TRANSFER_PRIVS) Corresponding, Controller no longer SENDS online-accounts that no longer have minting privs to other peers. Added some tests - more tests needed, e.g. for multiple transfers into the same account, or a test for minting post transfer for both sender & recipient.
This commit is contained in:
parent
5cd35e07d0
commit
1f7827b51f
@ -249,11 +249,10 @@ public class Account {
|
||||
this.repository.getAccountRepository().setLevel(accountData);
|
||||
}
|
||||
|
||||
public void setInitialLevel(int level) throws DataException {
|
||||
public void setBlocksMintedAdjustment(int blocksMintedAdjustment) throws DataException {
|
||||
AccountData accountData = this.buildAccountData();
|
||||
accountData.setLevel(level);
|
||||
accountData.setInitialLevel(level);
|
||||
this.repository.getAccountRepository().setInitialLevel(accountData);
|
||||
accountData.setBlocksMintedAdjustment(blocksMintedAdjustment);
|
||||
this.repository.getAccountRepository().setBlocksMintedAdjustment(accountData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,6 +46,7 @@ public enum ApiError {
|
||||
TRANSFORMATION_ERROR(127, 400),
|
||||
INVALID_PRIVATE_KEY(128, 400),
|
||||
INVALID_HEIGHT(129, 400),
|
||||
CANNOT_MINT(130, 400),
|
||||
|
||||
// WALLET
|
||||
WALLET_NO_EXISTS(201, 404),
|
||||
|
@ -37,6 +37,7 @@ 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.PrivateKeyAccount;
|
||||
import org.qora.account.PublicKeyAccount;
|
||||
import org.qora.api.ApiError;
|
||||
import org.qora.api.ApiErrors;
|
||||
import org.qora.api.ApiException;
|
||||
@ -243,7 +244,7 @@ public class AdminResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE, ApiError.CANNOT_MINT})
|
||||
public String addMintingAccount(String seed58) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
byte[] seed = Base58.decode(seed58.trim());
|
||||
@ -252,9 +253,15 @@ public class AdminResource {
|
||||
PrivateKeyAccount mintingAccount = new PrivateKeyAccount(repository, seed);
|
||||
|
||||
// Qortal: account must derive to known reward-share public key
|
||||
if (!repository.getAccountRepository().isRewardSharePublicKey(mintingAccount.getPublicKey()))
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccount.getPublicKey());
|
||||
if (rewardShareData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
// Qortal: check reward-share's minting account is still allowed to mint
|
||||
PublicKeyAccount rewardShareMintingAccount = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
|
||||
if (!rewardShareMintingAccount.canMint())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.CANNOT_MINT);
|
||||
|
||||
MintingAccountData mintingAccountData = new MintingAccountData(seed);
|
||||
|
||||
repository.getAccountRepository().save(mintingAccountData);
|
||||
|
@ -1207,6 +1207,9 @@ public class Block {
|
||||
|
||||
// Block rewards go before transactions processed
|
||||
processBlockRewards();
|
||||
|
||||
// Give transaction fees to minter/reward-share account(s)
|
||||
rewardTransactionFees();
|
||||
}
|
||||
|
||||
// Process transactions (we'll link them to this block after saving the block itself)
|
||||
@ -1215,10 +1218,6 @@ public class Block {
|
||||
// Group-approval transactions
|
||||
processGroupApprovalTransactions();
|
||||
|
||||
if (this.blockData.getHeight() > 1)
|
||||
// Give transaction fees to minter/reward-share account(s)
|
||||
rewardTransactionFees();
|
||||
|
||||
// Process AT fees and save AT states into repository
|
||||
processAtFeesAndStates();
|
||||
|
||||
@ -1261,15 +1260,15 @@ public class Block {
|
||||
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||
}
|
||||
|
||||
// We are only interested in accounts that are NOT founders and NOT already highest level
|
||||
// We are only interested in accounts that are NOT already highest level
|
||||
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||
List<ExpandedAccount> candidateAccounts = expandedAccounts.stream().filter(expandedAccount -> !isFounder.test(expandedAccount) && getAccountData.apply(expandedAccount).getLevel() < maximumLevel).collect(Collectors.toList());
|
||||
List<ExpandedAccount> candidateAccounts = expandedAccounts.stream().filter(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 effectiveBlocksMinted = cumulativeBlocksByLevel.get(accountData.getInitialLevel()) + accountData.getBlocksMinted();
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
|
||||
|
||||
for (int newLevel = maximumLevel; newLevel > 0; --newLevel)
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
@ -1426,9 +1425,8 @@ public class Block {
|
||||
public void orphan() throws DataException {
|
||||
LOGGER.trace(() -> String.format("Orphaning block %d", this.blockData.getHeight()));
|
||||
|
||||
if (this.blockData.getHeight() > 1)
|
||||
// Deduct any transaction fees from minter/reward-share account(s)
|
||||
deductTransactionFees();
|
||||
// Return AT fees and delete AT states from repository
|
||||
orphanAtFeesAndStates();
|
||||
|
||||
// Orphan, and unlink, transactions from this block
|
||||
orphanTransactionsFromBlock();
|
||||
@ -1437,6 +1435,12 @@ public class Block {
|
||||
orphanGroupApprovalTransactions();
|
||||
|
||||
if (this.blockData.getHeight() > 1) {
|
||||
// Invalidate expandedAccounts as they may have changed due to orphaning TRANSFER_PRIVS transactions, etc.
|
||||
this.cachedExpandedAccounts = null;
|
||||
|
||||
// Deduct any transaction fees from minter/reward-share account(s)
|
||||
deductTransactionFees();
|
||||
|
||||
// Block rewards removed after transactions undone
|
||||
orphanBlockRewards();
|
||||
|
||||
@ -1444,9 +1448,6 @@ public class Block {
|
||||
decreaseAccountLevels();
|
||||
}
|
||||
|
||||
// Return AT fees and delete AT states from repository
|
||||
orphanAtFeesAndStates();
|
||||
|
||||
// Delete orphaned balances
|
||||
this.repository.getAccountRepository().deleteBalancesFromHeight(this.blockData.getHeight());
|
||||
|
||||
@ -1574,15 +1575,15 @@ public class Block {
|
||||
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||
}
|
||||
|
||||
// We are only interested in accounts that are NOT founders and NOT already lowest level
|
||||
// We are only interested in accounts that are NOT already lowest level
|
||||
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||
List<ExpandedAccount> candidateAccounts = expandedAccounts.stream().filter(expandedAccount -> !isFounder.test(expandedAccount) && getAccountData.apply(expandedAccount).getLevel() > 0).collect(Collectors.toList());
|
||||
List<ExpandedAccount> candidateAccounts = expandedAccounts.stream().filter(expandedAccount -> getAccountData.apply(expandedAccount).getLevel() > 0).collect(Collectors.toList());
|
||||
|
||||
for (int c = 0; c < candidateAccounts.size(); ++c) {
|
||||
ExpandedAccount expandedAccount = candidateAccounts.get(c);
|
||||
final AccountData accountData = getAccountData.apply(expandedAccount);
|
||||
|
||||
final int effectiveBlocksMinted = cumulativeBlocksByLevel.get(accountData.getInitialLevel()) + accountData.getBlocksMinted();
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
|
||||
|
||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
|
@ -110,6 +110,27 @@ public class BlockMinter extends Thread {
|
||||
if (mintingAccountsData.isEmpty())
|
||||
continue;
|
||||
|
||||
// Disregard minting accounts that are no longer valid, e.g. by transfer/loss of founder flag or account level
|
||||
// Note that minting accounts are actually reward-shares in Qortal
|
||||
Iterator<MintingAccountData> madi = mintingAccountsData.iterator();
|
||||
while (madi.hasNext()) {
|
||||
MintingAccountData mintingAccountData = madi.next();
|
||||
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
|
||||
if (rewardShareData == null) {
|
||||
// Reward-share doesn't even exist - probably not a good sign
|
||||
madi.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
PublicKeyAccount mintingAccount = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
|
||||
if (!mintingAccount.canMint()) {
|
||||
// Minting-account component of reward-share can no longer mint - disregard
|
||||
madi.remove();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
List<Peer> peers = Network.getInstance().getUniqueHandshakedPeers();
|
||||
BlockData lastBlockData = blockRepository.getLastBlock();
|
||||
|
||||
|
@ -34,6 +34,7 @@ import org.qora.block.BlockMinter;
|
||||
import org.qora.controller.Synchronizer.SynchronizationResult;
|
||||
import org.qora.crypto.Crypto;
|
||||
import org.qora.data.account.MintingAccountData;
|
||||
import org.qora.data.account.RewardShareData;
|
||||
import org.qora.data.block.BlockData;
|
||||
import org.qora.data.block.BlockSummaryData;
|
||||
import org.qora.data.network.OnlineAccountData;
|
||||
@ -1263,8 +1264,12 @@ public class Controller extends Thread {
|
||||
List<OnlineAccountData> peersOnlineAccounts = onlineAccountsMessage.getOnlineAccounts();
|
||||
LOGGER.trace(() -> String.format("Received %d online accounts from %s", peersOnlineAccounts.size(), peer));
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
for (OnlineAccountData onlineAccountData : peersOnlineAccounts)
|
||||
this.verifyAndAddAccount(onlineAccountData);
|
||||
this.verifyAndAddAccount(repository, onlineAccountData);
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while verifying online accounts from peer %s", peer), e);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
@ -1277,13 +1282,13 @@ public class Controller extends Thread {
|
||||
|
||||
// Utilities
|
||||
|
||||
private void verifyAndAddAccount(OnlineAccountData onlineAccountData) {
|
||||
PublicKeyAccount otherAccount = new PublicKeyAccount(null, onlineAccountData.getPublicKey());
|
||||
|
||||
private void verifyAndAddAccount(Repository repository, OnlineAccountData onlineAccountData) throws DataException {
|
||||
final Long now = NTP.getTime();
|
||||
if (now == null)
|
||||
return;
|
||||
|
||||
PublicKeyAccount otherAccount = new PublicKeyAccount(repository, onlineAccountData.getPublicKey());
|
||||
|
||||
// Check timestamp is 'recent' here
|
||||
if (Math.abs(onlineAccountData.getTimestamp() - now) > ONLINE_TIMESTAMP_MODULUS * 2) {
|
||||
LOGGER.trace(() -> String.format("Rejecting online account %s with out of range timestamp %d", otherAccount.getAddress(), onlineAccountData.getTimestamp()));
|
||||
@ -1297,6 +1302,21 @@ public class Controller extends Thread {
|
||||
return;
|
||||
}
|
||||
|
||||
// Qortal: check online account is actually reward-share
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(onlineAccountData.getPublicKey());
|
||||
if (rewardShareData == null) {
|
||||
// Reward-share doesn't even exist - probably not a good sign
|
||||
LOGGER.trace(() -> String.format("Rejecting unknown online reward-share public key %s", Base58.encode(onlineAccountData.getPublicKey())));
|
||||
return;
|
||||
}
|
||||
|
||||
PublicKeyAccount mintingAccount = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
|
||||
if (!mintingAccount.canMint()) {
|
||||
// Minting-account component of reward-share can no longer mint - disregard
|
||||
LOGGER.trace(() -> String.format("Rejecting online reward-share with non-minting account %s", mintingAccount.getAddress()));
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this.onlineAccounts) {
|
||||
OnlineAccountData existingAccountData = this.onlineAccounts.stream().filter(account -> Arrays.equals(account.getPublicKey(), onlineAccountData.getPublicKey())).findFirst().orElse(null);
|
||||
|
||||
@ -1401,8 +1421,19 @@ public class Controller extends Thread {
|
||||
while (iterator.hasNext()) {
|
||||
MintingAccountData mintingAccountData = iterator.next();
|
||||
|
||||
if (!repository.getAccountRepository().isRewardSharePublicKey(mintingAccountData.getPublicKey()))
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
|
||||
if (rewardShareData == null) {
|
||||
// Reward-share doesn't even exist - probably not a good sign
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
PublicKeyAccount mintingAccount = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
|
||||
if (!mintingAccount.canMint()) {
|
||||
// Minting-account component of reward-share can no longer mint - disregard
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue trying to fetch minting accounts: %s", e.getMessage()));
|
||||
|
@ -15,9 +15,9 @@ public class AccountData {
|
||||
protected byte[] publicKey;
|
||||
protected int defaultGroupId;
|
||||
protected int flags;
|
||||
protected int initialLevel;
|
||||
protected int level;
|
||||
protected int blocksMinted;
|
||||
protected int blocksMintedAdjustment;
|
||||
|
||||
// Constructors
|
||||
|
||||
@ -25,15 +25,15 @@ public class AccountData {
|
||||
protected AccountData() {
|
||||
}
|
||||
|
||||
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, int initialLevel, int level, int blocksMinted) {
|
||||
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, int level, int blocksMinted, int blocksMintedAdjustment) {
|
||||
this.address = address;
|
||||
this.reference = reference;
|
||||
this.publicKey = publicKey;
|
||||
this.defaultGroupId = defaultGroupId;
|
||||
this.flags = flags;
|
||||
this.initialLevel = initialLevel;
|
||||
this.level = level;
|
||||
this.blocksMinted = blocksMinted;
|
||||
this.blocksMintedAdjustment = blocksMintedAdjustment;
|
||||
}
|
||||
|
||||
public AccountData(String address) {
|
||||
@ -78,14 +78,6 @@ public class AccountData {
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
public int getInitialLevel() {
|
||||
return this.initialLevel;
|
||||
}
|
||||
|
||||
public void setInitialLevel(int level) {
|
||||
this.initialLevel = level;
|
||||
}
|
||||
|
||||
public int getLevel() {
|
||||
return this.level;
|
||||
}
|
||||
@ -102,6 +94,14 @@ public class AccountData {
|
||||
this.blocksMinted = blocksMinted;
|
||||
}
|
||||
|
||||
public int getBlocksMintedAdjustment() {
|
||||
return this.blocksMintedAdjustment;
|
||||
}
|
||||
|
||||
public void setBlocksMintedAdjustment(int blocksMintedAdjustment) {
|
||||
this.blocksMintedAdjustment = blocksMintedAdjustment;
|
||||
}
|
||||
|
||||
// Comparison
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,112 @@
|
||||
package org.qora.data.transaction;
|
||||
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
|
||||
import org.qora.transaction.Transaction.TransactionType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@Schema(allOf = { TransactionData.class })
|
||||
public class TransferPrivsTransactionData extends TransactionData {
|
||||
|
||||
// Properties
|
||||
@Schema(example = "sender_public_key")
|
||||
private byte[] senderPublicKey;
|
||||
|
||||
private String recipient;
|
||||
|
||||
// No need to ever expose this via API
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
private Integer previousSenderFlags;
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
private Integer previousRecipientFlags;
|
||||
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
private Integer previousSenderBlocksMintedAdjustment;
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
private Integer previousSenderBlocksMinted;
|
||||
|
||||
// Constructors
|
||||
|
||||
// For JAXB
|
||||
protected TransferPrivsTransactionData() {
|
||||
super(TransactionType.TRANSFER_PRIVS);
|
||||
}
|
||||
|
||||
public void afterUnmarshal(Unmarshaller u, Object parent) {
|
||||
this.creatorPublicKey = this.senderPublicKey;
|
||||
}
|
||||
|
||||
/** Constructs using data from repository. */
|
||||
public TransferPrivsTransactionData(BaseTransactionData baseTransactionData, String recipient,
|
||||
Integer previousSenderFlags, Integer previousRecipientFlags,
|
||||
Integer previousSenderBlocksMintedAdjustment, Integer previousSenderBlocksMinted) {
|
||||
super(TransactionType.TRANSFER_PRIVS, baseTransactionData);
|
||||
|
||||
this.senderPublicKey = baseTransactionData.creatorPublicKey;
|
||||
this.recipient = recipient;
|
||||
|
||||
this.previousSenderFlags = previousSenderFlags;
|
||||
this.previousRecipientFlags = previousRecipientFlags;
|
||||
|
||||
this.previousSenderBlocksMintedAdjustment = previousSenderBlocksMintedAdjustment;
|
||||
this.previousSenderBlocksMinted = previousSenderBlocksMinted;
|
||||
}
|
||||
|
||||
/** Constructs using data from network/API. */
|
||||
public TransferPrivsTransactionData(BaseTransactionData baseTransactionData, String recipient) {
|
||||
this(baseTransactionData, recipient, null, null, null, null);
|
||||
}
|
||||
|
||||
// Getters/setters
|
||||
|
||||
public byte[] getSenderPublicKey() {
|
||||
return this.senderPublicKey;
|
||||
}
|
||||
|
||||
public String getRecipient() {
|
||||
return this.recipient;
|
||||
}
|
||||
|
||||
public Integer getPreviousSenderFlags() {
|
||||
return this.previousSenderFlags;
|
||||
}
|
||||
|
||||
public void setPreviousSenderFlags(Integer previousSenderFlags) {
|
||||
this.previousSenderFlags = previousSenderFlags;
|
||||
}
|
||||
|
||||
public Integer getPreviousRecipientFlags() {
|
||||
return this.previousRecipientFlags;
|
||||
}
|
||||
|
||||
public void setPreviousRecipientFlags(Integer previousRecipientFlags) {
|
||||
this.previousRecipientFlags = previousRecipientFlags;
|
||||
}
|
||||
|
||||
public Integer getPreviousSenderBlocksMintedAdjustment() {
|
||||
return this.previousSenderBlocksMintedAdjustment;
|
||||
}
|
||||
|
||||
public void setPreviousSenderBlocksMintedAdjustment(Integer previousSenderBlocksMintedAdjustment) {
|
||||
this.previousSenderBlocksMintedAdjustment = previousSenderBlocksMintedAdjustment;
|
||||
}
|
||||
|
||||
public Integer getPreviousSenderBlocksMinted() {
|
||||
return this.previousSenderBlocksMinted;
|
||||
}
|
||||
|
||||
public void setPreviousSenderBlocksMinted(Integer previousSenderBlocksMinted) {
|
||||
this.previousSenderBlocksMinted = previousSenderBlocksMinted;
|
||||
}
|
||||
|
||||
}
|
@ -69,11 +69,11 @@ public interface AccountRepository {
|
||||
public void setLevel(AccountData accountData) throws DataException;
|
||||
|
||||
/**
|
||||
* Saves account's initial & current level, and public key if present, in repository.
|
||||
* Saves account's blocks-minted adjustment, and public key if present, in repository.
|
||||
* <p>
|
||||
* Note: ignores other fields like last reference, default groupID.
|
||||
*/
|
||||
public void setInitialLevel(AccountData accountData) throws DataException;
|
||||
public void setBlocksMintedAdjustment(AccountData accountData) throws DataException;
|
||||
|
||||
/**
|
||||
* Saves account's minted block count and public key if present, in repository.
|
||||
|
@ -27,7 +27,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public AccountData getAccount(String address) throws DataException {
|
||||
String sql = "SELECT reference, public_key, default_group_id, flags, initial_level, level, blocks_minted FROM Accounts WHERE account = ?";
|
||||
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment FROM Accounts WHERE account = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
|
||||
if (resultSet == null)
|
||||
@ -37,11 +37,11 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
byte[] publicKey = resultSet.getBytes(2);
|
||||
int defaultGroupId = resultSet.getInt(3);
|
||||
int flags = resultSet.getInt(4);
|
||||
int initialLevel = resultSet.getInt(5);
|
||||
int level = resultSet.getInt(6);
|
||||
int blocksMinted = resultSet.getInt(7);
|
||||
int level = resultSet.getInt(5);
|
||||
int blocksMinted = resultSet.getInt(6);
|
||||
int blocksMintedAdjustment = resultSet.getInt(7);
|
||||
|
||||
return new AccountData(address, reference, publicKey, defaultGroupId, flags, initialLevel, level, blocksMinted);
|
||||
return new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch account info from repository", e);
|
||||
}
|
||||
@ -49,7 +49,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public List<AccountData> getFlaggedAccounts(int mask) throws DataException {
|
||||
String sql = "SELECT reference, public_key, default_group_id, flags, initial_level, level, blocks_minted, account FROM Accounts WHERE BITAND(flags, ?) != 0";
|
||||
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, account FROM Accounts WHERE BITAND(flags, ?) != 0";
|
||||
|
||||
List<AccountData> accounts = new ArrayList<>();
|
||||
|
||||
@ -62,12 +62,12 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
byte[] publicKey = resultSet.getBytes(2);
|
||||
int defaultGroupId = resultSet.getInt(3);
|
||||
int flags = resultSet.getInt(4);
|
||||
int initialLevel = resultSet.getInt(5);
|
||||
int level = resultSet.getInt(6);
|
||||
int blocksMinted = resultSet.getInt(7);
|
||||
int level = resultSet.getInt(5);
|
||||
int blocksMinted = resultSet.getInt(6);
|
||||
int blocksMintedAdjustment = resultSet.getInt(7);
|
||||
String address = resultSet.getString(8);
|
||||
|
||||
accounts.add(new AccountData(address, reference, publicKey, defaultGroupId, flags, initialLevel, level, blocksMinted));
|
||||
accounts.add(new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment));
|
||||
} while (resultSet.next());
|
||||
|
||||
return accounts;
|
||||
@ -239,11 +239,11 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInitialLevel(AccountData accountData) throws DataException {
|
||||
public void setBlocksMintedAdjustment(AccountData accountData) throws DataException {
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
|
||||
|
||||
saveHelper.bind("account", accountData.getAddress()).bind("level", accountData.getLevel())
|
||||
.bind("initial_level", accountData.getInitialLevel());
|
||||
saveHelper.bind("account", accountData.getAddress())
|
||||
.bind("blocks_minted_adjustment", accountData.getBlocksMintedAdjustment());
|
||||
|
||||
byte[] publicKey = accountData.getPublicKey();
|
||||
if (publicKey != null)
|
||||
@ -252,7 +252,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to save account's initial level into repository", e);
|
||||
throw new DataException("Unable to save account's blocks minted adjustment into repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@ -905,6 +906,23 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("ALTER TABLE GroupInvites ALTER COLUMN expiry SET NULL");
|
||||
break;
|
||||
|
||||
case 64:
|
||||
// TRANSFER_PRIVS transaction
|
||||
stmt.execute("CREATE TABLE TransferPrivsTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, "
|
||||
+ "previous_sender_flags INT, previous_recipient_flags INT, "
|
||||
+ "previous_sender_blocks_minted_adjustment INT, previous_sender_blocks_minted INT, "
|
||||
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
|
||||
|
||||
// Convert Account's "initial_level" to "blocks_minted_adjustment"
|
||||
stmt.execute("ALTER TABLE Accounts ADD blocks_minted_adjustment INT NOT NULL DEFAULT 0");
|
||||
|
||||
List<Integer> blocksByLevel = BlockChain.getInstance().getBlocksNeededByLevel();
|
||||
for (int bbli = 0; bbli < blocksByLevel.size(); ++bbli)
|
||||
stmt.execute("UPDATE Accounts SET blocks_minted_adjustment = " + blocksByLevel.get(bbli) + " WHERE initial_level = " + (bbli + 1));
|
||||
|
||||
stmt.execute("ALTER TABLE Accounts DROP initial_level");
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@ -0,0 +1,69 @@
|
||||
package org.qora.repository.hsqldb.transaction;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.qora.data.transaction.BaseTransactionData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.data.transaction.TransferPrivsTransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.hsqldb.HSQLDBRepository;
|
||||
import org.qora.repository.hsqldb.HSQLDBSaver;
|
||||
|
||||
public class HSQLDBTransferPrivsTransactionRepository extends HSQLDBTransactionRepository {
|
||||
|
||||
public HSQLDBTransferPrivsTransactionRepository(HSQLDBRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
|
||||
String sql = "SELECT recipient, previous_sender_flags, previous_recipient_flags, previous_sender_blocks_minted_adjustment, previous_sender_blocks_minted FROM TransferPrivsTransactions WHERE signature = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
String recipient = resultSet.getString(1);
|
||||
|
||||
Integer previousSenderFlags = resultSet.getInt(2);
|
||||
if (previousSenderFlags == 0 && resultSet.wasNull())
|
||||
previousSenderFlags = null;
|
||||
|
||||
Integer previousRecipientFlags = resultSet.getInt(3);
|
||||
if (previousRecipientFlags == 0 && resultSet.wasNull())
|
||||
previousRecipientFlags = null;
|
||||
|
||||
Integer previousSenderBlocksMintedAdjustment = resultSet.getInt(4);
|
||||
if (previousSenderBlocksMintedAdjustment == 0 && resultSet.wasNull())
|
||||
previousSenderBlocksMintedAdjustment = null;
|
||||
|
||||
Integer previousSenderBlocksMinted = resultSet.getInt(5);
|
||||
if (previousSenderBlocksMinted == 0 && resultSet.wasNull())
|
||||
previousSenderBlocksMinted = null;
|
||||
|
||||
return new TransferPrivsTransactionData(baseTransactionData, recipient, previousSenderFlags, previousRecipientFlags, previousSenderBlocksMintedAdjustment, previousSenderBlocksMinted);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch transfer privs transaction from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(TransactionData transactionData) throws DataException {
|
||||
TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
|
||||
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("TransferPrivsTransactions");
|
||||
saveHelper.bind("signature", transferPrivsTransactionData.getSignature()).bind("sender", transferPrivsTransactionData.getSenderPublicKey())
|
||||
.bind("recipient", transferPrivsTransactionData.getRecipient())
|
||||
.bind("previous_sender_flags", transferPrivsTransactionData.getPreviousSenderFlags())
|
||||
.bind("previous_recipient_flags", transferPrivsTransactionData.getPreviousRecipientFlags())
|
||||
.bind("previous_sender_blocks_minted_adjustment", transferPrivsTransactionData.getPreviousSenderBlocksMintedAdjustment())
|
||||
.bind("previous_sender_blocks_minted", transferPrivsTransactionData.getPreviousSenderBlocksMinted());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to save transfer privs transaction into repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -7,6 +7,7 @@ import java.util.List;
|
||||
import org.qora.account.Account;
|
||||
import org.qora.account.GenesisAccount;
|
||||
import org.qora.asset.Asset;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.data.transaction.AccountLevelTransactionData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
@ -91,7 +92,12 @@ public class AccountLevelTransaction extends Transaction {
|
||||
this.repository.getTransactionRepository().save(accountLevelTransactionData);
|
||||
|
||||
// Set account's initial level
|
||||
target.setInitialLevel(this.accountLevelTransactionData.getLevel());
|
||||
target.setLevel(this.accountLevelTransactionData.getLevel());
|
||||
|
||||
// Set account's blocks minted adjustment
|
||||
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
int blocksMintedAdjustment = cumulativeBlocksByLevel.get(this.accountLevelTransactionData.getLevel());
|
||||
target.setBlocksMintedAdjustment(blocksMintedAdjustment);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -116,23 +116,25 @@ public class RewardShareTransaction extends Transaction {
|
||||
if (this.rewardShareTransactionData.getSharePercent().compareTo(MAX_SHARE) > 0)
|
||||
return ValidationResult.INVALID_REWARD_SHARE_PERCENT;
|
||||
|
||||
PublicKeyAccount creator = getCreator();
|
||||
|
||||
// Check reward-share public key is correct length
|
||||
if (this.rewardShareTransactionData.getRewardSharePublicKey().length != Transformer.PUBLIC_KEY_LENGTH)
|
||||
return ValidationResult.INVALID_PUBLIC_KEY;
|
||||
|
||||
Account recipient = getRecipient();
|
||||
if (!Crypto.isValidAddress(recipient.getAddress()))
|
||||
// Check recipient address is valid
|
||||
if (!Crypto.isValidAddress(this.rewardShareTransactionData.getRecipient()))
|
||||
return ValidationResult.INVALID_ADDRESS;
|
||||
|
||||
// Creator themselves needs to be allowed to mint
|
||||
if (!creator.canMint())
|
||||
PublicKeyAccount creator = getCreator();
|
||||
Account recipient = getRecipient();
|
||||
final boolean isCancellingSharePercent = this.rewardShareTransactionData.getSharePercent().compareTo(BigDecimal.ZERO) < 0;
|
||||
|
||||
// Creator themselves needs to be allowed to mint (unless cancelling)
|
||||
if (!isCancellingSharePercent && !creator.canMint())
|
||||
return ValidationResult.NOT_MINTING_ACCOUNT;
|
||||
|
||||
// Qortal: special rules in play depending whether recipient is also minter
|
||||
final boolean isRecipientAlsoMinter = creator.getAddress().equals(recipient.getAddress());
|
||||
if (!isRecipientAlsoMinter && !creator.canRewardShare())
|
||||
if (!isCancellingSharePercent && !isRecipientAlsoMinter && !creator.canRewardShare())
|
||||
return ValidationResult.ACCOUNT_CANNOT_REWARD_SHARE;
|
||||
|
||||
// Look up any existing reward-share (using transaction's reward-share public key)
|
||||
@ -143,13 +145,11 @@ public class RewardShareTransaction extends Transaction {
|
||||
if (existingRewardShareData != null && !this.doesRewardShareMatch(existingRewardShareData))
|
||||
return ValidationResult.INVALID_PUBLIC_KEY;
|
||||
|
||||
final boolean isSharePercentNegative = this.rewardShareTransactionData.getSharePercent().compareTo(BigDecimal.ZERO) < 0;
|
||||
|
||||
if (existingRewardShareData == null) {
|
||||
// This is a new reward-share
|
||||
|
||||
// No point starting a new reward-share with negative share (i.e. delete reward-share)
|
||||
if (isSharePercentNegative)
|
||||
// Deleting a non-existent reward-share makes no sense
|
||||
if (isCancellingSharePercent)
|
||||
return ValidationResult.INVALID_REWARD_SHARE_PERCENT;
|
||||
|
||||
// Check the minting account hasn't reach maximum number of reward-shares
|
||||
@ -160,7 +160,7 @@ public class RewardShareTransaction extends Transaction {
|
||||
// This transaction intends to modify/terminate an existing reward-share
|
||||
|
||||
// Modifying an existing self-share is pointless and forbidden (due to 0 fee). Deleting self-share is OK though.
|
||||
if (isRecipientAlsoMinter && !isSharePercentNegative)
|
||||
if (isRecipientAlsoMinter && !isCancellingSharePercent)
|
||||
return ValidationResult.INVALID_REWARD_SHARE_PERCENT;
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,8 @@ public abstract class Transaction {
|
||||
ACCOUNT_FLAGS(36, false),
|
||||
ENABLE_FORGING(37, false),
|
||||
REWARD_SHARE(38, false),
|
||||
ACCOUNT_LEVEL(39, false);
|
||||
ACCOUNT_LEVEL(39, false),
|
||||
TRANSFER_PRIVS(40, false);
|
||||
|
||||
public final int value;
|
||||
public final boolean needsApproval;
|
||||
@ -926,11 +927,14 @@ public abstract class Transaction {
|
||||
* Returns whether transaction can be processed.
|
||||
* <p>
|
||||
* With group-approval, even if a transaction had valid values
|
||||
* when submitted, by the time it is approved dependency might
|
||||
* when submitted, by the time it is approved these values
|
||||
* might become invalid, e.g. because dependencies might
|
||||
* have changed.
|
||||
* <p>
|
||||
* For example, with UPDATE_ASSET, the asset owner might have
|
||||
* changed between submission and approval.
|
||||
* changed between submission and approval and so the transaction
|
||||
* is invalid because the previous owner (as specified in the
|
||||
* transaction) no longer has permission to update the asset.
|
||||
*
|
||||
* @throws DataException
|
||||
*/
|
||||
|
250
src/main/java/org/qora/transaction/TransferPrivsTransaction.java
Normal file
250
src/main/java/org/qora/transaction/TransferPrivsTransaction.java
Normal file
@ -0,0 +1,250 @@
|
||||
package org.qora.transaction;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qora.account.Account;
|
||||
import org.qora.account.PublicKeyAccount;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.crypto.Crypto;
|
||||
import org.qora.data.account.AccountData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.data.transaction.TransferPrivsTransactionData;
|
||||
import org.qora.repository.AccountRepository;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
|
||||
public class TransferPrivsTransaction extends Transaction {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(TransferPrivsTransaction.class);
|
||||
|
||||
// Properties
|
||||
private TransferPrivsTransactionData transferPrivsTransactionData;
|
||||
|
||||
// Constructors
|
||||
|
||||
public TransferPrivsTransaction(Repository repository, TransactionData transactionData) {
|
||||
super(repository, transactionData);
|
||||
|
||||
this.transferPrivsTransactionData = (TransferPrivsTransactionData) this.transactionData;
|
||||
}
|
||||
|
||||
// More information
|
||||
|
||||
@Override
|
||||
public List<Account> getRecipientAccounts() throws DataException {
|
||||
return Collections.singletonList(new Account(this.repository, transferPrivsTransactionData.getRecipient()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInvolved(Account account) throws DataException {
|
||||
String address = account.getAddress();
|
||||
|
||||
if (address.equals(this.getSender().getAddress()))
|
||||
return true;
|
||||
|
||||
if (address.equals(transferPrivsTransactionData.getRecipient()))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal getAmount(Account account) throws DataException {
|
||||
String address = account.getAddress();
|
||||
BigDecimal amount = BigDecimal.ZERO.setScale(8);
|
||||
String senderAddress = this.getSender().getAddress();
|
||||
|
||||
if (address.equals(senderAddress))
|
||||
amount = amount.subtract(this.transactionData.getFee());
|
||||
|
||||
return amount;
|
||||
}
|
||||
|
||||
// Navigation
|
||||
|
||||
public Account getSender() throws DataException {
|
||||
return new PublicKeyAccount(this.repository, this.transferPrivsTransactionData.getSenderPublicKey());
|
||||
}
|
||||
|
||||
public Account getRecipient() throws DataException {
|
||||
return new Account(this.repository, this.transferPrivsTransactionData.getRecipient());
|
||||
}
|
||||
|
||||
// Processing
|
||||
|
||||
@Override
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// Check recipient address is valid
|
||||
if (!Crypto.isValidAddress(this.transferPrivsTransactionData.getRecipient()))
|
||||
return ValidationResult.INVALID_ADDRESS;
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
Account sender = this.getSender();
|
||||
Account recipient = this.getRecipient();
|
||||
|
||||
int senderFlags = sender.getFlags(); // Sender must exist so we always expect a result
|
||||
Integer recipientFlags = recipient.getFlags(); // Recipient might not exist yet, so null possible
|
||||
|
||||
// Save prior values
|
||||
this.transferPrivsTransactionData.setPreviousSenderFlags(senderFlags);
|
||||
this.transferPrivsTransactionData.setPreviousRecipientFlags(recipientFlags);
|
||||
|
||||
// Combine sender & recipient flags for recipient
|
||||
if (recipientFlags != null)
|
||||
senderFlags |= recipientFlags;
|
||||
|
||||
recipient.setFlags(senderFlags);
|
||||
|
||||
// Clear sender's flags
|
||||
sender.setFlags(0);
|
||||
|
||||
// Combine blocks minted counts/adjustments
|
||||
final AccountRepository accountRepository = this.repository.getAccountRepository();
|
||||
|
||||
AccountData senderData = accountRepository.getAccount(sender.getAddress());
|
||||
int sendersBlocksMinted = senderData.getBlocksMinted();
|
||||
int sendersBlocksMintedAdjustment = senderData.getBlocksMintedAdjustment();
|
||||
|
||||
AccountData recipientData = accountRepository.getAccount(recipient.getAddress());
|
||||
int recipientBlocksMinted = recipientData != null ? recipientData.getBlocksMinted() : 0;
|
||||
int recipientBlocksMintedAdjustment = recipientData != null ? recipientData.getBlocksMintedAdjustment() : 0;
|
||||
|
||||
// Save prior values
|
||||
this.transferPrivsTransactionData.setPreviousSenderBlocksMinted(sendersBlocksMinted);
|
||||
this.transferPrivsTransactionData.setPreviousSenderBlocksMintedAdjustment(sendersBlocksMintedAdjustment);
|
||||
|
||||
// Combine blocks minted
|
||||
recipientData.setBlocksMinted(recipientBlocksMinted + sendersBlocksMinted);
|
||||
accountRepository.setMintedBlockCount(recipientData);
|
||||
recipientData.setBlocksMintedAdjustment(recipientBlocksMintedAdjustment + sendersBlocksMintedAdjustment);
|
||||
accountRepository.setBlocksMintedAdjustment(recipientData);
|
||||
|
||||
// Determine new recipient level based on blocks
|
||||
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||
final int effectiveBlocksMinted = recipientData.getBlocksMinted() + recipientData.getBlocksMintedAdjustment();
|
||||
|
||||
for (int newLevel = maximumLevel; newLevel > 0; --newLevel)
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
if (newLevel > recipientData.getLevel()) {
|
||||
// Account has increased in level!
|
||||
recipientData.setLevel(newLevel);
|
||||
accountRepository.setLevel(recipientData);
|
||||
LOGGER.trace(() -> String.format("TRANSFER_PRIVS recipient %s bumped to level %d", recipientData.getAddress(), recipientData.getLevel()));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset sender's level
|
||||
sender.setLevel(0);
|
||||
|
||||
// Reset sender's blocks minted count & adjustment
|
||||
senderData.setBlocksMinted(0);
|
||||
accountRepository.setMintedBlockCount(senderData);
|
||||
senderData.setBlocksMintedAdjustment(0);
|
||||
accountRepository.setBlocksMintedAdjustment(senderData);
|
||||
|
||||
// Save this transaction
|
||||
this.repository.getTransactionRepository().save(this.transferPrivsTransactionData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processReferencesAndFees() throws DataException {
|
||||
super.processReferencesAndFees();
|
||||
|
||||
// If recipient has no last-reference then use this transaction's signature as last-reference so they can spend their block rewards
|
||||
Account recipient = new Account(this.repository, transferPrivsTransactionData.getRecipient());
|
||||
if (recipient.getLastReference() == null)
|
||||
recipient.setLastReference(transferPrivsTransactionData.getSignature());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void orphan() throws DataException {
|
||||
Account sender = this.getSender();
|
||||
Account recipient = this.getRecipient();
|
||||
|
||||
final AccountRepository accountRepository = this.repository.getAccountRepository();
|
||||
|
||||
AccountData senderData = accountRepository.getAccount(sender.getAddress());
|
||||
AccountData recipientData = accountRepository.getAccount(recipient.getAddress());
|
||||
|
||||
// Restore sender's flags
|
||||
senderData.setFlags(this.transferPrivsTransactionData.getPreviousSenderFlags());
|
||||
accountRepository.setFlags(senderData);
|
||||
|
||||
// Restore recipient's flags
|
||||
recipientData.setFlags(this.transferPrivsTransactionData.getPreviousRecipientFlags());
|
||||
accountRepository.setFlags(recipientData);
|
||||
|
||||
// Clean values in transaction data
|
||||
this.transferPrivsTransactionData.setPreviousSenderFlags(null);
|
||||
this.transferPrivsTransactionData.setPreviousRecipientFlags(null);
|
||||
|
||||
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||
|
||||
// Restore sender's block minted count/adjustment
|
||||
senderData.setBlocksMinted(this.transferPrivsTransactionData.getPreviousSenderBlocksMinted());
|
||||
accountRepository.setMintedBlockCount(senderData);
|
||||
senderData.setBlocksMintedAdjustment(this.transferPrivsTransactionData.getPreviousSenderBlocksMintedAdjustment());
|
||||
accountRepository.setBlocksMintedAdjustment(senderData);
|
||||
|
||||
// Recalculate sender's level
|
||||
int effectiveBlocksMinted = senderData.getBlocksMinted() + senderData.getBlocksMintedAdjustment();
|
||||
for (int newLevel = maximumLevel; newLevel > 0; --newLevel)
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
// Account level
|
||||
senderData.setLevel(newLevel);
|
||||
accountRepository.setLevel(senderData);
|
||||
LOGGER.trace(() -> String.format("TRANSFER_PRIVS sender %s reset to level %d", senderData.getAddress(), senderData.getLevel()));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Restore recipient block minted count/adjustment
|
||||
recipientData.setBlocksMinted(recipientData.getBlocksMinted() - this.transferPrivsTransactionData.getPreviousSenderBlocksMinted());
|
||||
accountRepository.setMintedBlockCount(recipientData);
|
||||
recipientData.setBlocksMintedAdjustment(recipientData.getBlocksMintedAdjustment() - this.transferPrivsTransactionData.getPreviousSenderBlocksMintedAdjustment());
|
||||
accountRepository.setBlocksMintedAdjustment(recipientData);
|
||||
|
||||
// Recalculate recipient's level
|
||||
effectiveBlocksMinted = recipientData.getBlocksMinted() + recipientData.getBlocksMintedAdjustment();
|
||||
for (int newLevel = maximumLevel; newLevel > 0; --newLevel)
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
// Account level
|
||||
recipientData.setLevel(newLevel);
|
||||
accountRepository.setLevel(recipientData);
|
||||
LOGGER.trace(() -> String.format("TRANSFER_PRIVS recipient %s reset to level %d", recipientData.getAddress(), recipientData.getLevel()));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Clear values in transaction data
|
||||
this.transferPrivsTransactionData.setPreviousSenderBlocksMinted(null);
|
||||
this.transferPrivsTransactionData.setPreviousSenderBlocksMintedAdjustment(null);
|
||||
|
||||
// Save this transaction
|
||||
this.repository.getTransactionRepository().save(this.transferPrivsTransactionData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void orphanReferencesAndFees() throws DataException {
|
||||
super.orphanReferencesAndFees();
|
||||
|
||||
// If recipient didn't have a last-reference prior to this transaction then remove it
|
||||
Account recipient = new Account(this.repository, transferPrivsTransactionData.getRecipient());
|
||||
if (Arrays.equals(recipient.getLastReference(), transferPrivsTransactionData.getSignature()))
|
||||
recipient.setLastReference(null);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
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.TransactionData;
|
||||
import org.qora.data.transaction.TransferPrivsTransactionData;
|
||||
import org.qora.transaction.Transaction.TransactionType;
|
||||
import org.qora.transform.TransformationException;
|
||||
import org.qora.utils.Serialization;
|
||||
|
||||
public class TransferPrivsTransactionTransformer extends TransactionTransformer {
|
||||
|
||||
// Property lengths
|
||||
private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH;
|
||||
|
||||
private static final int EXTRAS_LENGTH = RECIPIENT_LENGTH;
|
||||
|
||||
protected static final TransactionLayout layout;
|
||||
|
||||
static {
|
||||
layout = new TransactionLayout();
|
||||
layout.add("txType: " + TransactionType.TRANSFER_PRIVS.valueString, TransformationType.INT);
|
||||
layout.add("timestamp", TransformationType.TIMESTAMP);
|
||||
layout.add("transaction's groupID", TransformationType.INT);
|
||||
layout.add("reference", TransformationType.SIGNATURE);
|
||||
layout.add("sender's public key", TransformationType.PUBLIC_KEY);
|
||||
layout.add("recipient", 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[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer);
|
||||
|
||||
String recipient = Serialization.deserializeAddress(byteBuffer);
|
||||
|
||||
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
|
||||
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
byteBuffer.get(signature);
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, senderPublicKey, fee, signature);
|
||||
|
||||
return new TransferPrivsTransactionData(baseTransactionData, recipient);
|
||||
}
|
||||
|
||||
public static int getDataLength(TransactionData transactionData) throws TransformationException {
|
||||
return getBaseLength(transactionData) + EXTRAS_LENGTH;
|
||||
}
|
||||
|
||||
public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
|
||||
try {
|
||||
TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
transformCommonBytes(transactionData, bytes);
|
||||
|
||||
Serialization.serializeAddress(bytes, transferPrivsTransactionData.getRecipient());
|
||||
|
||||
Serialization.serializeBigDecimal(bytes, transferPrivsTransactionData.getFee());
|
||||
|
||||
if (transferPrivsTransactionData.getSignature() != null)
|
||||
bytes.write(transferPrivsTransactionData.getSignature());
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException | ClassCastException e) {
|
||||
throw new TransformationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
218
src/test/java/org/qora/test/TransferPrivsTests.java
Normal file
218
src/test/java/org/qora/test/TransferPrivsTests.java
Normal file
@ -0,0 +1,218 @@
|
||||
package org.qora.test;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.data.account.AccountData;
|
||||
import org.qora.data.transaction.BaseTransactionData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.data.transaction.TransferPrivsTransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.test.common.BlockUtils;
|
||||
import org.qora.test.common.Common;
|
||||
import org.qora.test.common.TestAccount;
|
||||
import org.qora.test.common.TransactionUtils;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public class TransferPrivsTests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterTest() throws DataException {
|
||||
Common.orphanCheck();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAliceIntoDilbertTransferPrivs() throws DataException {
|
||||
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
TestAccount alice = Common.getTestAccount(repository, "alice");
|
||||
AccountData initialAliceData = repository.getAccountRepository().getAccount(alice.getAddress());
|
||||
|
||||
TestAccount dilbert = Common.getTestAccount(repository, "dilbert");
|
||||
AccountData initialDilbertData = repository.getAccountRepository().getAccount(dilbert.getAddress());
|
||||
|
||||
// Blocks needed by Alice to get Dilbert to next level post-combine
|
||||
final int expectedPostCombineLevel = initialDilbertData.getLevel() + 1;
|
||||
final int blocksNeeded = cumulativeBlocksByLevel.get(expectedPostCombineLevel) - initialDilbertData.getBlocksMinted() - initialDilbertData.getBlocksMintedAdjustment();
|
||||
|
||||
// Level we expect Alice to reach after minting above blocks
|
||||
int expectedLevel = 0;
|
||||
for (int newLevel = maximumLevel; newLevel > 0; --newLevel)
|
||||
if (blocksNeeded >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
expectedLevel = newLevel;
|
||||
break;
|
||||
}
|
||||
|
||||
// Mint enough blocks to bump recipient level when we combine accounts
|
||||
for (int bc = 0; bc < blocksNeeded; ++bc)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Check minting account has gained level
|
||||
assertEquals("minter level incorrect", expectedLevel, (int) alice.getLevel());
|
||||
|
||||
// Grab pre-combine versions of Alice and Dilbert data
|
||||
AccountData preCombineAliceData = repository.getAccountRepository().getAccount(alice.getAddress());
|
||||
AccountData preCombineDilbertData = repository.getAccountRepository().getAccount(dilbert.getAddress());
|
||||
assertEquals(expectedLevel, preCombineAliceData.getLevel());
|
||||
|
||||
// Combine Alice into Dilbert
|
||||
byte[] reference = alice.getLastReference();
|
||||
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
|
||||
int txGroupId = 0;
|
||||
BigDecimal fee = BigDecimal.ONE.setScale(8);
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, alice.getPublicKey(), fee, null);
|
||||
TransactionData transactionData = new TransferPrivsTransactionData(baseTransactionData, dilbert.getAddress());
|
||||
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
AccountData newAliceData = repository.getAccountRepository().getAccount(alice.getAddress());
|
||||
AccountData newDilbertData = repository.getAccountRepository().getAccount(dilbert.getAddress());
|
||||
|
||||
checkSenderCleared(newAliceData);
|
||||
|
||||
// Confirm recipient has bumped level
|
||||
assertEquals("recipient's level incorrect", expectedPostCombineLevel, newDilbertData.getLevel());
|
||||
|
||||
// Confirm recipient has gained sender's flags
|
||||
assertEquals("recipient's flags should be changed", initialAliceData.getFlags() | initialDilbertData.getFlags(), (int) newDilbertData.getFlags());
|
||||
|
||||
// Confirm recipient has increased minted block count
|
||||
assertEquals("recipient minted block count incorrect", initialDilbertData.getBlocksMinted() + initialAliceData.getBlocksMinted() + blocksNeeded + 1, newDilbertData.getBlocksMinted());
|
||||
|
||||
// Confirm recipient has increased minted block adjustment
|
||||
assertEquals("recipient minted block adjustment incorrect", initialDilbertData.getBlocksMintedAdjustment() + initialAliceData.getBlocksMintedAdjustment(), newDilbertData.getBlocksMintedAdjustment());
|
||||
|
||||
// Orphan previous block
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Sender checks...
|
||||
AccountData orphanedAliceData = repository.getAccountRepository().getAccount(alice.getAddress());
|
||||
checkAccountDataRestored("sender", preCombineAliceData, orphanedAliceData);
|
||||
|
||||
// Recipient checks...
|
||||
AccountData orphanedDilbertData = repository.getAccountRepository().getAccount(dilbert.getAddress());
|
||||
checkAccountDataRestored("recipient", preCombineDilbertData, orphanedDilbertData);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDilbertIntoAliceTransferPrivs() throws DataException {
|
||||
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
TestAccount alice = Common.getTestAccount(repository, "alice");
|
||||
AccountData initialAliceData = repository.getAccountRepository().getAccount(alice.getAddress());
|
||||
|
||||
TestAccount dilbert = Common.getTestAccount(repository, "dilbert");
|
||||
AccountData initialDilbertData = repository.getAccountRepository().getAccount(dilbert.getAddress());
|
||||
|
||||
// Blocks needed by Alice to get Alice to next level post-combine
|
||||
final int expectedPostCombineLevel = initialDilbertData.getLevel() + 1;
|
||||
final int blocksNeeded = cumulativeBlocksByLevel.get(expectedPostCombineLevel) - initialDilbertData.getBlocksMinted() - initialDilbertData.getBlocksMintedAdjustment();
|
||||
|
||||
// Level we expect Alice to reach after minting above blocks
|
||||
int expectedLevel = 0;
|
||||
for (int newLevel = maximumLevel; newLevel > 0; --newLevel)
|
||||
if (blocksNeeded >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
expectedLevel = newLevel;
|
||||
break;
|
||||
}
|
||||
|
||||
// Mint enough blocks to bump recipient level when we combine accounts
|
||||
for (int bc = 0; bc < blocksNeeded; ++bc)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Check minting account has gained level
|
||||
assertEquals("minter level incorrect", expectedLevel, (int) alice.getLevel());
|
||||
|
||||
// Grab pre-combine versions of Alice and Dilbert data
|
||||
AccountData preCombineAliceData = repository.getAccountRepository().getAccount(alice.getAddress());
|
||||
AccountData preCombineDilbertData = repository.getAccountRepository().getAccount(dilbert.getAddress());
|
||||
assertEquals(expectedLevel, preCombineAliceData.getLevel());
|
||||
|
||||
// Combine Dilbert into Alice
|
||||
byte[] reference = dilbert.getLastReference();
|
||||
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
|
||||
int txGroupId = 0;
|
||||
BigDecimal fee = BigDecimal.ONE.setScale(8);
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, dilbert.getPublicKey(), fee, null);
|
||||
TransactionData transactionData = new TransferPrivsTransactionData(baseTransactionData, alice.getAddress());
|
||||
|
||||
TransactionUtils.signAndMint(repository, transactionData, dilbert);
|
||||
|
||||
AccountData newAliceData = repository.getAccountRepository().getAccount(alice.getAddress());
|
||||
AccountData newDilbertData = repository.getAccountRepository().getAccount(dilbert.getAddress());
|
||||
|
||||
checkSenderCleared(newDilbertData);
|
||||
|
||||
// Confirm recipient has bumped level
|
||||
assertEquals("recipient's level incorrect", expectedPostCombineLevel, newAliceData.getLevel());
|
||||
|
||||
// Confirm recipient has gained sender's flags
|
||||
assertEquals("recipient's flags should be changed", initialAliceData.getFlags() | initialDilbertData.getFlags(), (int) newAliceData.getFlags());
|
||||
|
||||
// Confirm recipient has increased minted block count
|
||||
assertEquals("recipient minted block count incorrect", initialDilbertData.getBlocksMinted() + initialAliceData.getBlocksMinted() + blocksNeeded + 1, newAliceData.getBlocksMinted());
|
||||
|
||||
// Confirm recipient has increased minted block adjustment
|
||||
assertEquals("recipient minted block adjustment incorrect", initialDilbertData.getBlocksMintedAdjustment() + initialAliceData.getBlocksMintedAdjustment(), newAliceData.getBlocksMintedAdjustment());
|
||||
|
||||
// Orphan previous block
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Sender checks...
|
||||
AccountData orphanedDilbertData = repository.getAccountRepository().getAccount(dilbert.getAddress());
|
||||
checkAccountDataRestored("sender", preCombineDilbertData, orphanedDilbertData);
|
||||
|
||||
// Recipient checks...
|
||||
AccountData orphanedAliceData = repository.getAccountRepository().getAccount(alice.getAddress());
|
||||
checkAccountDataRestored("recipient", preCombineAliceData, orphanedAliceData);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkSenderCleared(AccountData senderAccountData) {
|
||||
// Confirm sender has zeroed flags
|
||||
assertEquals("sender's flags should be zeroed", 0, (int) senderAccountData.getFlags());
|
||||
|
||||
// Confirm sender has zeroed level
|
||||
assertEquals("sender's level should be zeroed", 0, (int) senderAccountData.getLevel());
|
||||
|
||||
// Confirm sender has zeroed minted block count
|
||||
assertEquals("sender's minted block count should be zeroed", 0, (int) senderAccountData.getBlocksMinted());
|
||||
|
||||
// Confirm sender has zeroed minted block adjustment
|
||||
assertEquals("sender's minted block adjustment should be zeroed", 0, (int) senderAccountData.getBlocksMintedAdjustment());
|
||||
}
|
||||
|
||||
private void checkAccountDataRestored(String accountName, AccountData expectedAccountData, AccountData actualAccountData) {
|
||||
// Confirm flags have been restored
|
||||
assertEquals(accountName + "'s flags weren't restored", expectedAccountData.getFlags(), actualAccountData.getFlags());
|
||||
|
||||
// Confirm minted blocks count
|
||||
assertEquals(accountName + "'s minted block count wasn't restored", expectedAccountData.getBlocksMinted(), actualAccountData.getBlocksMinted());
|
||||
|
||||
// Confirm minted block adjustment
|
||||
assertEquals(accountName + "'s minted block adjustment wasn't restored", expectedAccountData.getBlocksMintedAdjustment(), actualAccountData.getBlocksMintedAdjustment());
|
||||
|
||||
// Confirm level has been restored
|
||||
assertEquals(accountName + "'s level wasn't restored", expectedAccountData.getLevel(), actualAccountData.getLevel());
|
||||
}
|
||||
|
||||
}
|
@ -160,10 +160,10 @@ public class Common {
|
||||
AccountBalanceData initialBalance = initialBalances.get(i);
|
||||
AccountBalanceData remainingBalance = remainingBalances.get(i);
|
||||
|
||||
assertEquals("Remaining balance's asset differs", initialBalance.getAssetId(), remainingBalance.getAssetId());
|
||||
assertEquals("Remaining balance's address differs", initialBalance.getAddress(), remainingBalance.getAddress());
|
||||
assertEquals(initialBalance.getAddress() + " remaining balance's asset differs", initialBalance.getAssetId(), remainingBalance.getAssetId());
|
||||
|
||||
assertEqualBigDecimals("Remaining balance differs", initialBalance.getBalance(), remainingBalance.getBalance());
|
||||
assertEqualBigDecimals(initialBalance.getAddress() + " remaining balance differs", initialBalance.getBalance(), remainingBalance.getBalance());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
package org.qora.test.common.transaction;
|
||||
|
||||
import org.qora.account.PrivateKeyAccount;
|
||||
import org.qora.data.transaction.TransferPrivsTransactionData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
|
||||
public class TransferPrivsTestTransaction extends TestTransaction {
|
||||
|
||||
public static TransactionData randomTransaction(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException {
|
||||
String recipient = account.getAddress();
|
||||
|
||||
return new TransferPrivsTransactionData(generateBase(account), recipient);
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user