Higher account levels more likely to win blocks

Also:

RewardShareKeys app now supports only one arg (minter private key)
in self-reward-share mode, where recipient public key is derived
from minter private key.

Added methods to Account for returning 'effective' minting level
where minting level for founders is read from blockchain config.
(Or returns zero if unable to mint).

Changed two Block constructors into static methods that return
a new Block as there was way too much work being done to really
be called a constructor, especially with all the opportunities
to throw an exception too.

Main blockchain config updated to reflect near-launch version.

Added/changed blockchain weight tests to check block winning
based on higher account levels.
This commit is contained in:
catbref
2019-11-06 09:47:10 +00:00
parent ebc2ee6ea9
commit 00aee1458e
16 changed files with 399 additions and 138 deletions

View File

@@ -4,131 +4,182 @@ import static org.junit.Assert.*;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import org.qora.crypto.Crypto;
import org.qora.account.Account;
import org.qora.block.Block;
import org.qora.data.block.BlockSummaryData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.test.common.Common;
import org.qora.test.common.TestAccount;
import org.qora.transform.Transformer;
import org.qora.transform.block.BlockTransformer;
import org.junit.Before;
import org.junit.Test;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Longs;
public class ChainWeightTests extends Common {
public class ChainWeightTests {
private static final int ACCOUNTS_COUNT_SHIFT = Transformer.PUBLIC_KEY_LENGTH * 8;
private static final int CHAIN_WEIGHT_SHIFT = 8;
private static final Random RANDOM = new Random();
private static final BigInteger MAX_DISTANCE;
static {
byte[] maxValue = new byte[Transformer.PUBLIC_KEY_LENGTH];
Arrays.fill(maxValue, (byte) 0xFF);
MAX_DISTANCE = new BigInteger(1, maxValue);
@Before
public void beforeTest() throws DataException {
Common.useSettings("test-settings-v2-minting.json");
}
private static byte[] perturbPublicKey(int height, byte[] publicKey) {
return Crypto.digest(Bytes.concat(Longs.toByteArray(height), publicKey));
}
private static BigInteger calcKeyDistance(int parentHeight, byte[] parentGeneratorKey, byte[] publicKey) {
byte[] idealKey = perturbPublicKey(parentHeight, parentGeneratorKey);
byte[] perturbedKey = perturbPublicKey(parentHeight + 1, publicKey);
BigInteger keyDistance = MAX_DISTANCE.subtract(new BigInteger(idealKey).subtract(new BigInteger(perturbedKey)).abs());
return keyDistance;
}
private static BigInteger calcBlockWeight(int parentHeight, byte[] parentGeneratorKey, BlockSummaryData blockSummaryData) {
BigInteger keyDistance = calcKeyDistance(parentHeight, parentGeneratorKey, blockSummaryData.getMinterPublicKey());
BigInteger weight = BigInteger.valueOf(blockSummaryData.getOnlineAccountsCount()).shiftLeft(ACCOUNTS_COUNT_SHIFT).add(keyDistance);
return weight;
}
private static BigInteger calcChainWeight(int commonBlockHeight, byte[] commonBlockGeneratorKey, List<BlockSummaryData> blockSummaries) {
BigInteger cumulativeWeight = BigInteger.ZERO;
int parentHeight = commonBlockHeight;
byte[] parentGeneratorKey = commonBlockGeneratorKey;
for (BlockSummaryData blockSummaryData : blockSummaries) {
cumulativeWeight = cumulativeWeight.shiftLeft(CHAIN_WEIGHT_SHIFT).add(calcBlockWeight(parentHeight, parentGeneratorKey, blockSummaryData));
parentHeight = blockSummaryData.getHeight();
parentGeneratorKey = blockSummaryData.getMinterPublicKey();
}
return cumulativeWeight;
}
private static BlockSummaryData genBlockSummary(int height) {
byte[] generatorPublicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
RANDOM.nextBytes(generatorPublicKey);
private static BlockSummaryData genBlockSummary(Repository repository, int height) {
TestAccount testAccount = Common.getRandomTestAccount(repository, true);
byte[] minterPublicKey = testAccount.getPublicKey();
byte[] signature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
RANDOM.nextBytes(signature);
int onlineAccountsCount = RANDOM.nextInt(1000);
return new BlockSummaryData(height, signature, generatorPublicKey, onlineAccountsCount);
return new BlockSummaryData(height, signature, minterPublicKey, onlineAccountsCount);
}
private static List<BlockSummaryData> genBlockSummaries(int count, BlockSummaryData commonBlockSummary) {
private static List<BlockSummaryData> genBlockSummaries(Repository repository, int count, BlockSummaryData commonBlockSummary) {
List<BlockSummaryData> blockSummaries = new ArrayList<>();
blockSummaries.add(commonBlockSummary);
final int commonBlockHeight = commonBlockSummary.getHeight();
for (int i = 1; i <= count; ++i)
blockSummaries.add(genBlockSummary(commonBlockHeight + i));
blockSummaries.add(genBlockSummary(repository, commonBlockHeight + i));
return blockSummaries;
}
// Check that more online accounts beats a better key
@Test
public void testMoreAccountsBlock() {
final int parentHeight = 1;
final byte[] parentGeneratorKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
public void testMoreAccountsBlock() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
final int parentHeight = 1;
final byte[] parentMinterKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
int betterAccountsCount = 100;
int worseAccountsCount = 20;
int betterAccountsCount = 100;
int worseAccountsCount = 20;
byte[] betterKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
betterKey[0] = 0x41;
TestAccount betterAccount = Common.getTestAccount(repository, "bob-reward-share");
byte[] betterKey = betterAccount.getPublicKey();
int betterMinterLevel = Account.getRewardShareEffectiveMintingLevel(repository, betterKey);
byte[] worseKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
worseKey[0] = 0x23;
TestAccount worseAccount = Common.getTestAccount(repository, "dilbert-reward-share");
byte[] worseKey = worseAccount.getPublicKey();
int worseMinterLevel = Account.getRewardShareEffectiveMintingLevel(repository, worseKey);
BigInteger betterKeyDistance = calcKeyDistance(parentHeight, parentGeneratorKey, betterKey);
BigInteger worseKeyDistance = calcKeyDistance(parentHeight, parentGeneratorKey, worseKey);
assertEquals("hard-coded keys are wrong", 1, betterKeyDistance.compareTo(worseKeyDistance));
// This is to check that the hard-coded keys ARE actually better/worse as expected, before moving on testing more online accounts
BigInteger betterKeyDistance = Block.calcKeyDistance(parentHeight, parentMinterKey, betterKey, betterMinterLevel);
BigInteger worseKeyDistance = Block.calcKeyDistance(parentHeight, parentMinterKey, worseKey, worseMinterLevel);
assertEquals("hard-coded keys are wrong", 1, betterKeyDistance.compareTo(worseKeyDistance));
BlockSummaryData betterBlockSummary = new BlockSummaryData(parentHeight + 1, null, worseKey, betterAccountsCount);
BlockSummaryData worseBlockSummary = new BlockSummaryData(parentHeight + 1, null, betterKey, worseAccountsCount);
BlockSummaryData betterBlockSummary = new BlockSummaryData(parentHeight + 1, null, worseKey, betterAccountsCount);
BlockSummaryData worseBlockSummary = new BlockSummaryData(parentHeight + 1, null, betterKey, worseAccountsCount);
BigInteger betterBlockWeight = calcBlockWeight(parentHeight, parentGeneratorKey, betterBlockSummary);
BigInteger worseBlockWeight = calcBlockWeight(parentHeight, parentGeneratorKey, worseBlockSummary);
populateBlockSummaryMinterLevel(repository, betterBlockSummary);
populateBlockSummaryMinterLevel(repository, worseBlockSummary);
assertEquals("block weights are wrong", 1, betterBlockWeight.compareTo(worseBlockWeight));
BigInteger betterBlockWeight = Block.calcBlockWeight(parentHeight, parentMinterKey, betterBlockSummary);
BigInteger worseBlockWeight = Block.calcBlockWeight(parentHeight, parentMinterKey, worseBlockSummary);
assertEquals("block weights are wrong", 1, betterBlockWeight.compareTo(worseBlockWeight));
}
}
// Check that a longer chain beats a shorter chain
@Test
public void testLongerChain() {
final int commonBlockHeight = 1;
BlockSummaryData commonBlockSummary = genBlockSummary(commonBlockHeight);
byte[] commonBlockGeneratorKey = commonBlockSummary.getMinterPublicKey();
public void testLongerChain() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
final int commonBlockHeight = 1;
BlockSummaryData commonBlockSummary = genBlockSummary(repository, commonBlockHeight);
byte[] commonBlockGeneratorKey = commonBlockSummary.getMinterPublicKey();
List<BlockSummaryData> shorterChain = genBlockSummaries(3, commonBlockSummary);
List<BlockSummaryData> longerChain = genBlockSummaries(shorterChain.size() + 1, commonBlockSummary);
List<BlockSummaryData> shorterChain = genBlockSummaries(repository, 3, commonBlockSummary);
List<BlockSummaryData> longerChain = genBlockSummaries(repository, shorterChain.size() + 1, commonBlockSummary);
BigInteger shorterChainWeight = calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, shorterChain);
BigInteger longerChainWeight = calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, longerChain);
populateBlockSummariesMinterLevels(repository, shorterChain);
populateBlockSummariesMinterLevels(repository, longerChain);
assertEquals("longer chain should have greater weight", 1, longerChainWeight.compareTo(shorterChainWeight));
BigInteger shorterChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, shorterChain);
BigInteger longerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, longerChain);
assertEquals("longer chain should have greater weight", 1, longerChainWeight.compareTo(shorterChainWeight));
}
}
// Check that a higher level account wins more blocks
@Test
public void testMinterLevel() throws DataException {
testMinterLevels("chloe-reward-share", "bob-reward-share");
}
private void testMinterLevels(String betterMinterName, String worseMinterName) throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount betterAccount = Common.getTestAccount(repository, betterMinterName);
byte[] betterKey = betterAccount.getPublicKey();
int betterMinterLevel = Account.getRewardShareEffectiveMintingLevel(repository, betterKey);
TestAccount worseAccount = Common.getTestAccount(repository, worseMinterName);
byte[] worseKey = worseAccount.getPublicKey();
int worseMinterLevel = Account.getRewardShareEffectiveMintingLevel(repository, worseKey);
// Check hard-coded accounts have expected better/worse levels
assertTrue("hard-coded accounts have wrong relative minting levels", betterMinterLevel > worseMinterLevel);
Random random = new Random();
final int onlineAccountsCount = 100;
int betterAccountWins = 0;
int worseAccountWins = 0;
byte[] parentSignature = new byte[64];
random.nextBytes(parentSignature);
for (int parentHeight = 1; parentHeight < 1000; ++parentHeight) {
byte[] blockSignature = new byte[64];
random.nextBytes(blockSignature);
BlockSummaryData betterBlockSummary = new BlockSummaryData(parentHeight + 1, blockSignature, worseKey, onlineAccountsCount);
BlockSummaryData worseBlockSummary = new BlockSummaryData(parentHeight + 1, blockSignature, betterKey, onlineAccountsCount);
populateBlockSummaryMinterLevel(repository, betterBlockSummary);
populateBlockSummaryMinterLevel(repository, worseBlockSummary);
BigInteger betterBlockWeight = Block.calcBlockWeight(parentHeight, parentSignature, betterBlockSummary);
BigInteger worseBlockWeight = Block.calcBlockWeight(parentHeight, parentSignature, worseBlockSummary);
if (betterBlockWeight.compareTo(worseBlockWeight) >= 0)
++betterAccountWins;
else
++worseAccountWins;
parentSignature = blockSignature;
}
assertTrue("Account with better minting level didn't win more blocks", betterAccountWins > worseAccountWins);
}
}
// Check that a higher level account wins more blocks
@Test
public void testFounderMinterLevel() throws DataException {
testMinterLevels("alice-reward-share", "dilbert-reward-share");
}
private void populateBlockSummariesMinterLevels(Repository repository, List<BlockSummaryData> blockSummaries) throws DataException {
for (int i = 0; i < blockSummaries.size(); ++i) {
BlockSummaryData blockSummary = blockSummaries.get(i);
populateBlockSummaryMinterLevel(repository, blockSummary);
}
}
private void populateBlockSummaryMinterLevel(Repository repository, BlockSummaryData blockSummary) throws DataException {
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockSummary.getMinterPublicKey());
assertNotSame("effective minter level should not be zero", 0, minterLevel);
blockSummary.setMinterLevel(minterLevel);
}
}

View File

@@ -1155,7 +1155,7 @@ public class TransactionTests extends Common {
}
private Block forgeBlock(TransactionData transactionData) throws DataException {
Block block = new Block(repository, parentBlockData, generator);
Block block = Block.mint(repository, parentBlockData, generator);
block.addTransaction(transactionData);
block.sign();
return block;

View File

@@ -10,6 +10,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -71,12 +72,30 @@ public class Common {
// Alice reward-share with herself. Private key is reward-share private key, derived from Alice's private and public keys.
testAccountsByName.put("alice-reward-share", new TestAccount(null, "alice-reward-share", "1CeDCg9TSdBwJNGVTGG7pCKsvsyyoEcaVXYvDT1Xb9f", true));
// Bob self-share
testAccountsByName.put("bob-reward-share", new TestAccount(null, "bob-reward-share", "975G6DJX2bhkq2dawxxDbNe5DcT33LbGto5tRueKVRDx", true));
// Chloe self-share
testAccountsByName.put("chloe-reward-share", new TestAccount(null, "chloe-reward-share", "2paayAXTbGmdLtJ7tNxY93bhPnWZwNYwk15KA37Sw5yS", true));
// Dilbert self-share
testAccountsByName.put("dilbert-reward-share", new TestAccount(null, "dilbert-reward-share", "C3DqD3K9bZDqxwLBroXc2NgL2SRJrif1mcAW7zNMUg9", true));
}
public static TestAccount getTestAccount(Repository repository, String name) {
return new TestAccount(repository, testAccountsByName.get(name));
}
public static TestAccount getRandomTestAccount(Repository repository, Boolean includeRewardShare) {
List<TestAccount> testAccounts = new ArrayList<>(testAccountsByName.values());
if (includeRewardShare != null)
testAccounts.removeIf(account -> account.isRewardShare != includeRewardShare);
Random random = new Random();
int index = random.nextInt(testAccounts.size());
return testAccounts.get(index);
}
public static List<TestAccount> getTestAccounts(Repository repository) {
return testAccountsByName.values().stream().map(account -> new TestAccount(repository, account)).collect(Collectors.toList());
}

View File

@@ -8,6 +8,7 @@
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
"maxRewardSharesPerMintingAccount": 20,
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
"rewardsByHeight": [
@@ -23,6 +24,7 @@
{ "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }

View File

@@ -0,0 +1,69 @@
{
"isTestChain": true,
"blockTimestampMargin": 500,
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
"maxRewardSharesPerMintingAccount": 20,
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },
{ "height": 21, "reward": 1 }
],
"sharesByLevel": [
{ "levels": [ 1, 2 ], "share": 0.05 },
{ "levels": [ 3, 4 ], "share": 0.10 },
{ "levels": [ 5, 6 ], "share": 0.15 },
{ "levels": [ 7, 8 ], "share": 0.20 },
{ "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
],
"featureTriggers": {
"messageHeight": 0,
"atHeight": 0,
"assetsTimestamp": 0,
"votingTimestamp": 0,
"arbitraryTimestamp": 0,
"powfixTimestamp": 0,
"v2Timestamp": 0,
"newAssetPricingTimestamp": 0,
"groupApprovalTimestamp": 0
},
"genesisInfo": {
"version": 4,
"timestamp": 0,
"transactions": [
{ "type": "ISSUE_ASSET", "owner": "QcFmNxSArv5tWEzCtTKb2Lqc5QkKuQ7RNs", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
{ "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" },
{ "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" },
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" },
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" },
{ "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 },
{ "type": "ACCOUNT_LEVEL", "target": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "level": 1 },
{ "type": "REWARD_SHARE", "minterPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "rewardSharePublicKey": "CcABzvk26TFEHG7Yok84jxyd4oBtLkx8RJdGFVz2csvp", "sharePercent": 100 },
{ "type": "ACCOUNT_LEVEL", "target": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "level": 8 },
{ "type": "REWARD_SHARE", "minterPublicKey": "7KNBj2MnEb6zq1vvKY1q8G2Voctcc2Z1X4avFyEH2eJC", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "rewardSharePublicKey": "6bnEKqZbsCSWryUQnbBT9Umufdu3CapFvxfAni6afhFb", "sharePercent": 100 },
{ "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 5 },
{ "type": "REWARD_SHARE", "minterPublicKey": "CGAedAQU91SR73iqoYtss6NAsra284SShXnDWvRXqR4G", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "rewardSharePublicKey": "4QafENiQCCDCnbXgcZfiyCu9qWqZ6YEciXAyFb4TT8YQ", "sharePercent": 100 }
]
}
}

View File

@@ -8,6 +8,7 @@
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
"maxRewardSharesPerMintingAccount": 20,
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
"rewardsByHeight": [

View File

@@ -8,6 +8,7 @@
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
"maxRewardSharesPerMintingAccount": 20,
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
"rewardsByHeight": [

View File

@@ -0,0 +1,6 @@
{
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-minting.json",
"wipeUnconfirmedOnStart": false,
"minPeers": 0
}