Work on granting forging rights

Move hard-coded forging tiers to blockchain config.
Tests for granting forging rights.

Added API call to list top block forgers.

Fixed typo with Reward[s]ByHeight class name.
This commit is contained in:
catbref 2019-04-19 09:55:04 +01:00
parent 93230e9704
commit d33ffee3ba
15 changed files with 364 additions and 59 deletions

View File

@ -21,11 +21,6 @@ public class Account {
private static final Logger LOGGER = LogManager.getLogger(Account.class);
public static final int TIER1_FORGING_MASK = 0x1;
public static final int TIER2_FORGING_MASK = 0x2;
public static final int TIER3_FORGING_MASK = 0x4;
public static final int FORGING_MASK = TIER1_FORGING_MASK | TIER2_FORGING_MASK | TIER3_FORGING_MASK;
public static final int ADDRESS_LENGTH = 25;
protected Repository repository;

View File

@ -0,0 +1,19 @@
package org.qora.account;
import org.qora.block.BlockChain;
import org.qora.repository.DataException;
/** Relating to whether accounts can forge. */
public class Forging {
/** Returns mask for account flags for forging bits. */
public static int getForgingMask() {
return (1 << BlockChain.getInstance().getForgingTiers().size()) - 1;
}
public static boolean canForge(Account account) throws DataException {
Integer flags = account.getFlags();
return flags != null && (flags & getForgingMask()) != 0;
}
}

View File

@ -0,0 +1,20 @@
package org.qora.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class BlockForgeSummary {
public String address;
public int blockCount;
protected BlockForgeSummary() {
}
public BlockForgeSummary(String address, int blockCount) {
this.address = address;
this.blockCount = blockCount;
}
}

View File

@ -28,6 +28,7 @@ import org.qora.api.ApiError;
import org.qora.api.ApiErrors;
import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory;
import org.qora.api.model.BlockForgeSummary;
import org.qora.block.Block;
import org.qora.crypto.Crypto;
import org.qora.data.account.AccountData;
@ -570,6 +571,36 @@ public class BlocksResource {
}
}
@GET
@Path("/forgers")
@Operation(
summary = "Show summary of block forgers",
responses = {
@ApiResponse(
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = BlockForgeSummary.class
)
)
)
)
}
)
public List<BlockForgeSummary> getBlockForgers(@Parameter(
ref = "limit"
) @QueryParam("limit") Integer limit, @Parameter(
ref = "offset"
) @QueryParam("offset") Integer offset, @Parameter(
ref = "reverse"
) @QueryParam("reverse") Boolean reverse) {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().getBlockForgers(limit, offset, reverse);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/enableforging")
@Operation(

View File

@ -19,7 +19,7 @@ import org.qora.account.PrivateKeyAccount;
import org.qora.account.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.at.AT;
import org.qora.block.BlockChain.RewardsByHeight;
import org.qora.block.BlockChain.RewardByHeight;
import org.qora.crypto.Crypto;
import org.qora.data.account.ProxyForgerData;
import org.qora.data.at.ATData;
@ -1191,7 +1191,7 @@ public class Block {
}
protected BigDecimal getRewardAtHeight(int ourHeight) {
List<RewardsByHeight> rewardsByHeight = BlockChain.getInstance().getBlockRewardsByHeight();
List<RewardByHeight> rewardsByHeight = BlockChain.getInstance().getBlockRewardsByHeight();
// No rewards configured?
if (rewardsByHeight == null)

View File

@ -90,11 +90,20 @@ public class BlockChain {
private boolean oneNamePerAccount = false;
/** Block rewards by block height */
public static class RewardsByHeight {
public static class RewardByHeight {
public int height;
public BigDecimal reward;
}
List<RewardsByHeight> rewardsByHeight;
List<RewardByHeight> rewardsByHeight;
/** Forging right tiers */
public static class ForgingTier {
/** Minimum number of blocks forged before account can enable minting on other accounts. */
public int minBlocks;
/** Maximum number of other accounts that can be enabled. */
public int maxSubAccounts;
}
List<ForgingTier> forgingTiers;
// Constructors, etc.
@ -230,10 +239,14 @@ public class BlockChain {
return this.oneNamePerAccount;
}
public List<RewardsByHeight> getBlockRewardsByHeight() {
public List<RewardByHeight> getBlockRewardsByHeight() {
return this.rewardsByHeight;
}
public List<ForgingTier> getForgingTiers() {
return this.forgingTiers;
}
// Convenience methods for specific blockchain feature triggers
public long getMessageReleaseHeight() {

View File

@ -30,6 +30,10 @@ public class EnableForgingTransactionData extends TransactionData {
this.target = target;
}
public EnableForgingTransactionData(long timestamp, int groupId, byte[] reference, byte[] creatorPublicKey, String target, BigDecimal fee) {
this(timestamp, groupId, reference, creatorPublicKey, target, fee, null);
}
// Getters / setters
public String getTarget() {

View File

@ -2,6 +2,7 @@ package org.qora.repository;
import java.util.List;
import org.qora.api.model.BlockForgeSummary;
import org.qora.data.block.BlockData;
import org.qora.data.block.BlockTransactionData;
import org.qora.data.transaction.TransactionData;
@ -101,6 +102,11 @@ public interface BlockRepository {
*/
public int countForgedBlocks(byte[] publicKey) throws DataException;
/**
* Returns summaries of block forgers.
*/
public List<BlockForgeSummary> getBlockForgers(Integer limit, Integer offset, Boolean reverse) throws DataException;
/**
* Returns blocks with passed generator public key.
*/

View File

@ -6,6 +6,8 @@ import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.qora.api.model.BlockForgeSummary;
import org.qora.crypto.Crypto;
import org.qora.data.block.BlockData;
import org.qora.data.block.BlockTransactionData;
import org.qora.data.transaction.TransactionData;
@ -158,6 +160,33 @@ public class HSQLDBBlockRepository implements BlockRepository {
}
}
@Override
public List<BlockForgeSummary> getBlockForgers(Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT generator, COUNT(signature) FROM Blocks GROUP BY generator ORDER BY COUNT(signature) ";
if (reverse != null && reverse)
sql += " DESC";
sql += HSQLDBRepository.limitOffsetSql(limit, offset);
List<BlockForgeSummary> summaries = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
if (resultSet == null)
return summaries;
do {
byte[] generator = resultSet.getBytes(1);
int count = resultSet.getInt(2);
summaries.add(new BlockForgeSummary(Crypto.toAddress(generator), count));
} while (resultSet.next());
return summaries;
} catch (SQLException e) {
throw new DataException("Unable to fetch generator's blocks from repository", e);
}
}
@Override
public List<BlockData> getBlocksWithGenerator(byte[] generatorPublicKey, Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE generator = ? ORDER BY height ";

View File

@ -6,8 +6,11 @@ import java.util.Collections;
import java.util.List;
import org.qora.account.Account;
import org.qora.account.Forging;
import org.qora.account.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.block.BlockChain;
import org.qora.block.BlockChain.ForgingTier;
import org.qora.data.transaction.EnableForgingTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
@ -15,12 +18,6 @@ import org.qora.repository.Repository;
public class EnableForgingTransaction extends Transaction {
public static final int TIER1_MIN_FORGED_BLOCKS = 50;
public static final int TIER1_MAX_ENABLED_ACCOUNTS = 5;
public static final int TIER2_MIN_FORGED_BLOCKS = 5;
public static final int TIER2_MAX_ENABLED_ACCOUNTS = 5;
// Properties
private EnableForgingTransactionData enableForgingTransactionData;
@ -80,39 +77,45 @@ public class EnableForgingTransaction extends Transaction {
if (creatorFlags == null)
return ValidationResult.INVALID_ADDRESS;
if ((creatorFlags & Account.FORGING_MASK) == 0)
if ((creatorFlags & Forging.getForgingMask()) == 0)
return ValidationResult.NO_FORGING_PERMISSION;
// Tier3 forgers can't enable further accounts
if ((creatorFlags & Account.TIER3_FORGING_MASK) != 0)
int forgingTierLevel = 0;
ForgingTier forgingTier = null;
List<ForgingTier> forgingTiers = BlockChain.getInstance().getForgingTiers();
for (forgingTierLevel = 0; forgingTierLevel < forgingTiers.size(); ++forgingTierLevel)
if ((creatorFlags & (1 << forgingTierLevel)) != 0) {
forgingTier = forgingTiers.get(forgingTierLevel);
break;
}
// forgingTier should not be null at this point
if (forgingTier == null)
return ValidationResult.NO_FORGING_PERMISSION;
// Final tier forgers can't enable further accounts
if (forgingTierLevel == forgingTiers.size() - 1)
return ValidationResult.FORGING_ENABLE_LIMIT;
Account target = getTarget();
// Target needs to NOT have ANY forging-enabled account flags set
Integer targetFlags = target.getFlags();
if (targetFlags != null && (targetFlags & Account.FORGING_MASK) != 0)
if (Forging.canForge(target))
return ValidationResult.FORGING_ALREADY_ENABLED;
// Has creator reached minimum requirements?
int numberForgedBlocks = this.repository.getBlockRepository().countForgedBlocks(creator.getPublicKey());
// Already gifted maximum number of forging rights?
int numberEnabledAccounts = this.repository.getAccountRepository().countForgingAccountsEnabledByAddress(creator.getAddress());
if (numberEnabledAccounts >= forgingTier.maxSubAccounts)
return ValidationResult.FORGING_ENABLE_LIMIT;
if ((creatorFlags & Account.TIER1_FORGING_MASK) != 0) {
// Tier1: minimum 2,500 forged blocks & max 50 accounts
if (numberForgedBlocks < TIER1_MIN_FORGED_BLOCKS)
// Not enough forged blocks to gift forging rights?
int numberForgedBlocks = this.repository.getBlockRepository().countForgedBlocks(creator.getPublicKey());
if (numberForgedBlocks < forgingTier.minBlocks)
return ValidationResult.FORGE_MORE_BLOCKS;
if (numberEnabledAccounts >= TIER1_MAX_ENABLED_ACCOUNTS)
return ValidationResult.FORGING_ENABLE_LIMIT;
} else if ((creatorFlags & Account.TIER2_FORGING_MASK) != 0) {
// Tier2: minimum 50 forged blocks & max 50 accounts
if (numberForgedBlocks < TIER2_MIN_FORGED_BLOCKS)
return ValidationResult.FORGE_MORE_BLOCKS;
if (numberEnabledAccounts >= TIER2_MAX_ENABLED_ACCOUNTS)
return ValidationResult.FORGING_ENABLE_LIMIT;
}
// Check fee is zero or positive
if (enableForgingTransactionData.getFee().compareTo(BigDecimal.ZERO) < 0)
@ -135,19 +138,16 @@ public class EnableForgingTransaction extends Transaction {
int creatorFlags = creator.getFlags();
int forgeBit = 0;
if ((creatorFlags & Account.TIER1_FORGING_MASK) != 0)
forgeBit = Account.TIER2_FORGING_MASK;
else
forgeBit = Account.TIER3_FORGING_MASK;
int forgeBit = creatorFlags & Forging.getForgingMask();
// Target's forging bit is next level from creator's
int targetForgeBit = forgeBit << 1;
Account target = getTarget();
Integer targetFlags = target.getFlags();
if (targetFlags == null)
targetFlags = 0;
targetFlags |= forgeBit;
targetFlags |= targetForgeBit;
target.setFlags(targetFlags);
target.setForgingEnabler(creator.getAddress());
@ -169,18 +169,15 @@ public class EnableForgingTransaction extends Transaction {
int creatorFlags = creator.getFlags();
int forgeBit = 0;
if ((creatorFlags & Account.TIER1_FORGING_MASK) != 0)
forgeBit = Account.TIER2_FORGING_MASK;
else
forgeBit = Account.TIER3_FORGING_MASK;
int forgeBit = creatorFlags & Forging.getForgingMask();
// Target's forging bit is next level from creator's
int targetForgeBit = forgeBit << 1;
Account target = getTarget();
int targetFlags = target.getFlags();
targetFlags &= ~forgeBit;
targetFlags &= ~targetForgeBit;
target.setFlags(targetFlags);
target.setForgingEnabler(null);

View File

@ -6,6 +6,7 @@ import java.util.Collections;
import java.util.List;
import org.qora.account.Account;
import org.qora.account.Forging;
import org.qora.account.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.crypto.Crypto;
@ -84,12 +85,8 @@ public class ProxyForgingTransaction extends Transaction {
PublicKeyAccount creator = getCreator();
// Creator needs to have at least one forging-enabled account flag set
Integer creatorFlags = creator.getFlags();
if (creatorFlags == null)
return ValidationResult.INVALID_ADDRESS;
if ((creatorFlags & Account.FORGING_MASK) == 0)
// Creator themselves needs to be allowed to forge
if (!Forging.canForge(creator))
return ValidationResult.NO_FORGING_PERMISSION;
// Check proxy public key is correct length

View File

@ -5,6 +5,8 @@ import java.util.HashMap;
import java.util.Map;
import org.qora.account.PrivateKeyAccount;
import org.qora.crypto.Crypto;
import org.qora.data.transaction.EnableForgingTransactionData;
import org.qora.data.transaction.PaymentTransactionData;
import org.qora.data.transaction.ProxyForgingTransactionData;
import org.qora.data.transaction.TransactionData;
@ -46,6 +48,30 @@ public class AccountUtils {
return proxyPrivateKey;
}
public static TransactionData createEnableForging(Repository repository, String forger, byte[] recipientPublicKey) throws DataException {
PrivateKeyAccount forgingAccount = Common.getTestAccount(repository, forger);
byte[] reference = forgingAccount.getLastReference();
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1000;
return new EnableForgingTransactionData(timestamp, txGroupId, reference, forgingAccount.getPublicKey(), Crypto.toAddress(recipientPublicKey), fee);
}
public static TransactionData createEnableForging(Repository repository, String forger, String recipient) throws DataException {
PrivateKeyAccount recipientAccount = Common.getTestAccount(repository, recipient);
return createEnableForging(repository, forger, recipientAccount.getPublicKey());
}
public static TransactionData enableForging(Repository repository, String forger, String recipient) throws DataException {
TransactionData transactionData = createEnableForging(repository, forger, recipient);
PrivateKeyAccount forgingAccount = Common.getTestAccount(repository, forger);
TransactionUtils.signAndForge(repository, transactionData, forgingAccount);
return transactionData;
}
public static Map<String, Map<Long, BigDecimal>> getBalances(Repository repository, long... assetIds) throws DataException {
Map<String, Map<Long, BigDecimal>> balances = new HashMap<>();

View File

@ -0,0 +1,163 @@
package org.qora.test.forging;
import static org.junit.Assert.*;
import java.util.Random;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qora.account.PrivateKeyAccount;
import org.qora.block.BlockChain;
import org.qora.block.BlockGenerator;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.test.common.AccountUtils;
import org.qora.test.common.Common;
import org.qora.test.common.TransactionUtils;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.ValidationResult;
public class GrantForgingTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@After
public void afterTest() throws DataException {
Common.orphanCheck();
}
@Test
public void testSimpleGrant() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount forgingAccount = Common.getTestAccount(repository, "alice");
TransactionData transactionData = AccountUtils.createEnableForging(repository, "alice", "bob");
Transaction transaction = Transaction.fromData(repository, transactionData);
transaction.sign(forgingAccount);
ValidationResult result = transaction.isValidUnconfirmed();
// Alice can't grant without forging minimum number of blocks
assertEquals(ValidationResult.FORGE_MORE_BLOCKS, result);
// Forge a load of blocks
int blocksNeeded = BlockChain.getInstance().getForgingTiers().get(0).minBlocks;
for (int i = 0; i < blocksNeeded; ++i)
BlockGenerator.generateTestingBlock(repository, forgingAccount);
// Alice should be able to grant now
result = transaction.isValidUnconfirmed();
assertEquals(ValidationResult.OK, result);
TransactionUtils.signAndForge(repository, transactionData, forgingAccount);
}
}
@Test
public void testMaxGrant() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount forgingAccount = Common.getTestAccount(repository, "alice");
TransactionData transactionData = AccountUtils.createEnableForging(repository, "alice", "bob");
Transaction transaction = Transaction.fromData(repository, transactionData);
transaction.sign(forgingAccount);
ValidationResult result = transaction.isValidUnconfirmed();
// Alice can't grant without forging minimum number of blocks
assertEquals(ValidationResult.FORGE_MORE_BLOCKS, result);
// Forge a load of blocks
int blocksNeeded = BlockChain.getInstance().getForgingTiers().get(0).minBlocks;
for (int i = 0; i < blocksNeeded; ++i)
BlockGenerator.generateTestingBlock(repository, forgingAccount);
// Alice should be able to grant up to 5 now
// Gift to random accounts
Random random = new Random();
for (int i = 0; i < 5; ++i) {
byte[] publicKey = new byte[32];
random.nextBytes(publicKey);
transactionData = AccountUtils.createEnableForging(repository, "alice", publicKey);
transaction = Transaction.fromData(repository, transactionData);
transaction.sign(forgingAccount);
result = transaction.isValidUnconfirmed();
assertEquals("Couldn't enable account #" + i, ValidationResult.OK, result);
TransactionUtils.signAndForge(repository, transactionData, forgingAccount);
}
// Alice's allocation used up
byte[] publicKey = new byte[32];
random.nextBytes(publicKey);
transactionData = AccountUtils.createEnableForging(repository, "alice", publicKey);
transaction = Transaction.fromData(repository, transactionData);
transaction.sign(forgingAccount);
result = transaction.isValidUnconfirmed();
assertEquals(ValidationResult.FORGING_ENABLE_LIMIT, result);
}
}
@Test
public void testFinalTier() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount aliceAccount = Common.getTestAccount(repository, "alice");
TransactionData transactionData = AccountUtils.createEnableForging(repository, "alice", "bob");
Transaction transaction = Transaction.fromData(repository, transactionData);
transaction.sign(aliceAccount);
ValidationResult result = transaction.isValidUnconfirmed();
// Alice can't grant without forging minimum number of blocks
assertEquals(ValidationResult.FORGE_MORE_BLOCKS, result);
// Forge a load of blocks
int blocksNeeded = BlockChain.getInstance().getForgingTiers().get(0).minBlocks;
for (int i = 0; i < blocksNeeded; ++i)
BlockGenerator.generateTestingBlock(repository, aliceAccount);
// Alice should be able to grant now
AccountUtils.enableForging(repository, "alice", "bob");
// Bob can't grant without forging minimum number of blocks
transactionData = AccountUtils.createEnableForging(repository, "bob", "chloe");
transaction = Transaction.fromData(repository, transactionData);
transaction.sign(aliceAccount);
result = transaction.isValidUnconfirmed();
assertEquals(ValidationResult.FORGE_MORE_BLOCKS, result);
// Bob needs to forge a load of blocks
PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
blocksNeeded = BlockChain.getInstance().getForgingTiers().get(1).minBlocks;
for (int i = 0; i < blocksNeeded; ++i)
BlockGenerator.generateTestingBlock(repository, bobAccount);
// Bob should be able to grant now
AccountUtils.enableForging(repository, "bob", "chloe");
// Chloe is final tier so shouldn't be able to grant
Random random = new Random();
byte[] publicKey = new byte[32];
random.nextBytes(publicKey);
transactionData = AccountUtils.createEnableForging(repository, "chloe", publicKey);
transaction = Transaction.fromData(repository, transactionData);
transaction.sign(aliceAccount);
result = transaction.isValidUnconfirmed();
assertEquals(ValidationResult.FORGING_ENABLE_LIMIT, result);
}
}
}

View File

@ -11,7 +11,7 @@ import org.junit.Test;
import org.qora.account.PrivateKeyAccount;
import org.qora.asset.Asset;
import org.qora.block.BlockChain;
import org.qora.block.BlockChain.RewardsByHeight;
import org.qora.block.BlockChain.RewardByHeight;
import org.qora.block.BlockGenerator;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
@ -54,11 +54,11 @@ public class RewardTests extends Common {
PrivateKeyAccount forgingAccount = Common.getTestAccount(repository, "alice");
List<RewardsByHeight> rewards = BlockChain.getInstance().getBlockRewardsByHeight();
List<RewardByHeight> rewards = BlockChain.getInstance().getBlockRewardsByHeight();
int rewardIndex = rewards.size() - 1;
RewardsByHeight rewardInfo = rewards.get(rewardIndex);
RewardByHeight rewardInfo = rewards.get(rewardIndex);
BigDecimal expectedBalance = initialBalances.get("alice").get(Asset.QORA);
for (int height = rewardInfo.height; height > 1; --height) {

View File

@ -30,6 +30,11 @@
{ "height": 11, "reward": 10 },
{ "height": 21, "reward": 1 }
],
"forgingTiers": [
{ "minBlocks": 5, "maxSubAccounts": 5 },
{ "minBlocks": 4, "maxSubAccounts": 3 },
{ "minBlocks": 0, "maxSubAccounts": 0 }
],
"featureTriggers": {
"messageHeight": 0,
"atHeight": 0,