diff --git a/src/main/java/org/qora/account/Account.java b/src/main/java/org/qora/account/Account.java index 40ccf5ab..9bb29f4e 100644 --- a/src/main/java/org/qora/account/Account.java +++ b/src/main/java/org/qora/account/Account.java @@ -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); } /** diff --git a/src/main/java/org/qora/api/ApiError.java b/src/main/java/org/qora/api/ApiError.java index 17a5912c..00caed35 100644 --- a/src/main/java/org/qora/api/ApiError.java +++ b/src/main/java/org/qora/api/ApiError.java @@ -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), diff --git a/src/main/java/org/qora/api/resource/AdminResource.java b/src/main/java/org/qora/api/resource/AdminResource.java index 73fd6146..b27bc326 100644 --- a/src/main/java/org/qora/api/resource/AdminResource.java +++ b/src/main/java/org/qora/api/resource/AdminResource.java @@ -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); diff --git a/src/main/java/org/qora/block/Block.java b/src/main/java/org/qora/block/Block.java index 29126f3e..5f2dd015 100644 --- a/src/main/java/org/qora/block/Block.java +++ b/src/main/java/org/qora/block/Block.java @@ -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 candidateAccounts = expandedAccounts.stream().filter(expandedAccount -> !isFounder.test(expandedAccount) && getAccountData.apply(expandedAccount).getLevel() < maximumLevel).collect(Collectors.toList()); + List 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 candidateAccounts = expandedAccounts.stream().filter(expandedAccount -> !isFounder.test(expandedAccount) && getAccountData.apply(expandedAccount).getLevel() > 0).collect(Collectors.toList()); + List 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)) { diff --git a/src/main/java/org/qora/block/BlockMinter.java b/src/main/java/org/qora/block/BlockMinter.java index 65ad6161..29752be7 100644 --- a/src/main/java/org/qora/block/BlockMinter.java +++ b/src/main/java/org/qora/block/BlockMinter.java @@ -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 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 peers = Network.getInstance().getUniqueHandshakedPeers(); BlockData lastBlockData = blockRepository.getLastBlock(); diff --git a/src/main/java/org/qora/controller/Controller.java b/src/main/java/org/qora/controller/Controller.java index 9dd9ec98..70961d11 100644 --- a/src/main/java/org/qora/controller/Controller.java +++ b/src/main/java/org/qora/controller/Controller.java @@ -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 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())); diff --git a/src/main/java/org/qora/data/account/AccountData.java b/src/main/java/org/qora/data/account/AccountData.java index f1a53deb..eca8d150 100644 --- a/src/main/java/org/qora/data/account/AccountData.java +++ b/src/main/java/org/qora/data/account/AccountData.java @@ -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 diff --git a/src/main/java/org/qora/data/transaction/TransferPrivsTransactionData.java b/src/main/java/org/qora/data/transaction/TransferPrivsTransactionData.java new file mode 100644 index 00000000..3d4ac28f --- /dev/null +++ b/src/main/java/org/qora/data/transaction/TransferPrivsTransactionData.java @@ -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; + } + +} diff --git a/src/main/java/org/qora/repository/AccountRepository.java b/src/main/java/org/qora/repository/AccountRepository.java index c15c8d19..276cc8cd 100644 --- a/src/main/java/org/qora/repository/AccountRepository.java +++ b/src/main/java/org/qora/repository/AccountRepository.java @@ -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. *

* 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. diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java index f9df6876..ea025ea3 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBAccountRepository.java @@ -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 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 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); } } diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java index c277bde2..02b6169d 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -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 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; diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransferPrivsTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransferPrivsTransactionRepository.java new file mode 100644 index 00000000..3763ba48 --- /dev/null +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransferPrivsTransactionRepository.java @@ -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); + } + } + +} diff --git a/src/main/java/org/qora/transaction/AccountLevelTransaction.java b/src/main/java/org/qora/transaction/AccountLevelTransaction.java index 8427ec68..145eaaa4 100644 --- a/src/main/java/org/qora/transaction/AccountLevelTransaction.java +++ b/src/main/java/org/qora/transaction/AccountLevelTransaction.java @@ -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 cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); + int blocksMintedAdjustment = cumulativeBlocksByLevel.get(this.accountLevelTransactionData.getLevel()); + target.setBlocksMintedAdjustment(blocksMintedAdjustment); } @Override diff --git a/src/main/java/org/qora/transaction/RewardShareTransaction.java b/src/main/java/org/qora/transaction/RewardShareTransaction.java index 6d9121bb..5cfa4ea1 100644 --- a/src/main/java/org/qora/transaction/RewardShareTransaction.java +++ b/src/main/java/org/qora/transaction/RewardShareTransaction.java @@ -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; } diff --git a/src/main/java/org/qora/transaction/Transaction.java b/src/main/java/org/qora/transaction/Transaction.java index 267f0cbd..3e789469 100644 --- a/src/main/java/org/qora/transaction/Transaction.java +++ b/src/main/java/org/qora/transaction/Transaction.java @@ -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. *

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

* 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 */ diff --git a/src/main/java/org/qora/transaction/TransferPrivsTransaction.java b/src/main/java/org/qora/transaction/TransferPrivsTransaction.java new file mode 100644 index 00000000..19c85c0e --- /dev/null +++ b/src/main/java/org/qora/transaction/TransferPrivsTransaction.java @@ -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 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 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 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); + } + +} diff --git a/src/main/java/org/qora/transform/transaction/TransferPrivsTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/TransferPrivsTransactionTransformer.java new file mode 100644 index 00000000..920bb23d --- /dev/null +++ b/src/main/java/org/qora/transform/transaction/TransferPrivsTransactionTransformer.java @@ -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); + } + } + +} diff --git a/src/test/java/org/qora/test/TransferPrivsTests.java b/src/test/java/org/qora/test/TransferPrivsTests.java new file mode 100644 index 00000000..9ff5a4fb --- /dev/null +++ b/src/test/java/org/qora/test/TransferPrivsTests.java @@ -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 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 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()); + } + +} diff --git a/src/test/java/org/qora/test/common/Common.java b/src/test/java/org/qora/test/common/Common.java index ee17ebb6..0ab7d5d3 100644 --- a/src/test/java/org/qora/test/common/Common.java +++ b/src/test/java/org/qora/test/common/Common.java @@ -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()); } } } diff --git a/src/test/java/org/qora/test/common/transaction/TransferPrivsTestTransaction.java b/src/test/java/org/qora/test/common/transaction/TransferPrivsTestTransaction.java new file mode 100644 index 00000000..bd0b3162 --- /dev/null +++ b/src/test/java/org/qora/test/common/transaction/TransferPrivsTestTransaction.java @@ -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); + } + +}