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:
catbref 2020-01-13 15:45:48 +00:00
parent 5cd35e07d0
commit 1f7827b51f
20 changed files with 913 additions and 73 deletions

View File

@ -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);
}
/**

View File

@ -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),

View File

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

View File

@ -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)) {

View File

@ -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();

View File

@ -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));
for (OnlineAccountData onlineAccountData : peersOnlineAccounts)
this.verifyAndAddAccount(onlineAccountData);
try (final Repository repository = RepositoryManager.getRepository()) {
for (OnlineAccountData onlineAccountData : peersOnlineAccounts)
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()));

View File

@ -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

View File

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

View File

@ -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.

View File

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

View File

@ -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;

View File

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

View File

@ -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

View File

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

View File

@ -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
*/

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

View File

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

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

View File

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

View File

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