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

@ -11,26 +11,29 @@ import org.qora.utils.Base58;
public class RewardShareKeys {
private static void usage() {
System.err.println("Usage: RewardShareKeys <private-key> <public-key>");
System.err.println("Usage: RewardShareKeys <minter-private-key> [<recipient-public-key>]");
System.err.println("Example: RewardShareKeys pYQ6DpQBJ2n72TCLJLScEvwhf3boxWy2kQEPynakwpj 6rNn9b3pYRrG9UKqzMWYZ9qa8F3Zgv2mVWrULGHUusb");
System.err.println("Example (self-share): RewardShareKeys pYQ6DpQBJ2n72TCLJLScEvwhf3boxWy2kQEPynakwpj");
System.exit(1);
}
public static void main(String[] args) {
if (args.length != 2)
if (args.length < 1 || args.length > 2)
usage();
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
PrivateKeyAccount privateAccount = new PrivateKeyAccount(null, Base58.decode(args[0]));
PublicKeyAccount publicAccount = new PublicKeyAccount(null, Base58.decode(args[1]));
PrivateKeyAccount minterAccount = new PrivateKeyAccount(null, Base58.decode(args[0]));
PublicKeyAccount recipientAccount = new PublicKeyAccount(null, args.length > 1 ? Base58.decode(args[1]) : minterAccount.getPublicKey());
byte[] rewardSharePrivateKey = privateAccount.getRewardSharePrivateKey(publicAccount.getPublicKey());
byte[] rewardSharePrivateKey = minterAccount.getRewardSharePrivateKey(recipientAccount.getPublicKey());
byte[] rewardSharePublicKey = PrivateKeyAccount.toPublicKey(rewardSharePrivateKey);
System.out.println(String.format("Private key account: %s", privateAccount.getAddress()));
System.out.println(String.format("Public key account: %s", publicAccount.getAddress()));
System.out.println(String.format("Minter account: %s", minterAccount.getAddress()));
System.out.println(String.format("Minter's public key: %s", Base58.encode(minterAccount.getPublicKey())));
System.out.println(String.format("Recipient account: %s", recipientAccount.getAddress()));
System.out.println(String.format("Reward-share private key: %s", Base58.encode(rewardSharePrivateKey)));
System.out.println(String.format("Reward-share public key: %s", Base58.encode(rewardSharePublicKey)));

View File

@ -9,6 +9,7 @@ import org.qora.block.Block;
import org.qora.block.BlockChain;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData;
import org.qora.data.account.RewardShareData;
import org.qora.data.block.BlockData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.BlockRepository;
@ -239,6 +240,7 @@ public class Account {
// Account level
/** Returns account's level (0+) or null if account not found in repository. */
public Integer getLevel() throws DataException {
return this.repository.getAccountRepository().getLevel(this.address);
}
@ -256,4 +258,43 @@ public class Account {
this.repository.getAccountRepository().setInitialLevel(accountData);
}
/**
* Returns 'effective' minting level, or zero if account does not exist/cannot mint.
* <p>
* For founder accounts, this returns "founderEffectiveMintingLevel" from blockchain config.
*
* @return 0+
* @throws DataException
*/
public int getEffectiveMintingLevel() throws DataException {
if (this.isFounder())
return BlockChain.getInstance().getFounderEffectiveMintingLevel();
Integer level = this.getLevel();
if (level == null)
return 0;
return level;
}
/**
* Returns 'effective' minting level, or zero if reward-share does not exist.
* <p>
* For founder accounts, this returns "founderEffectiveMintingLevel" from blockchain config.
*
* @param repository
* @param rewardSharePublicKey
* @return 0+
* @throws DataException
*/
public static int getRewardShareEffectiveMintingLevel(Repository repository, byte[] rewardSharePublicKey) throws DataException {
// Find actual minter and get their effective minting level
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(rewardSharePublicKey);
if (rewardShareData == null)
return 0;
PublicKeyAccount rewardShareMinter = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
return rewardShareMinter.getEffectiveMintingLevel();
}
}

View File

@ -206,22 +206,21 @@ public class Block {
// Constructors
/**
* Constructs Block-handling object without loading transactions and AT states.
* Constructs new Block without loading transactions and AT states.
* <p>
* Transactions and AT states are loaded on first call to getTransactions() or getATStates() respectively.
*
* @param repository
* @param blockData
* @throws DataException
*/
public Block(Repository repository, BlockData blockData) throws DataException {
public Block(Repository repository, BlockData blockData) {
this.repository = repository;
this.blockData = blockData;
this.minter = new PublicKeyAccount(repository, blockData.getMinterPublicKey());
}
/**
* Constructs Block-handling object using passed transaction and AT states.
* Constructs new Block using passed transaction and AT states.
* <p>
* This constructor typically used when receiving a serialized block over the network.
*
@ -229,9 +228,8 @@ public class Block {
* @param blockData
* @param transactions
* @param atStates
* @throws DataException
*/
public Block(Repository repository, BlockData blockData, List<TransactionData> transactions, List<ATStateData> atStates) throws DataException {
public Block(Repository repository, BlockData blockData, List<TransactionData> transactions, List<ATStateData> atStates) {
this(repository, blockData);
this.transactions = new ArrayList<>();
@ -252,7 +250,21 @@ public class Block {
}
/**
* Constructs Block-handling object with basic, initial values.
* Constructs new Block with empty transaction list, using passed minter account.
*
* @param repository
* @param blockData
* @param minter
*/
private Block(Repository repository, BlockData blockData, PrivateKeyAccount minter) {
this(repository, blockData);
this.minter = minter;
this.transactions = new ArrayList<>();
}
/**
* Mints new Block with basic, initial values.
* <p>
* This constructor typically used when minting a new block.
* <p>
@ -263,10 +275,7 @@ public class Block {
* @param minter
* @throws DataException
*/
public Block(Repository repository, BlockData parentBlockData, PrivateKeyAccount minter) throws DataException {
this.repository = repository;
this.minter = minter;
public static Block mint(Repository repository, BlockData parentBlockData, PrivateKeyAccount minter) throws DataException {
Block parentBlock = new Block(repository, parentBlockData);
int version = parentBlock.getNextBlockVersion();
@ -318,39 +327,46 @@ public class Block {
throw new DataException("Unable to calculate next block minter signature", e);
}
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey());
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey());
if (minterLevel == 0)
throw new IllegalStateException("Minter effective level returned zero?");
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
int transactionCount = 0;
byte[] transactionsSignature = null;
int height = parentBlockData.getHeight() + 1;
this.transactions = new ArrayList<>();
int atCount = 0;
BigDecimal atFees = BigDecimal.ZERO.setScale(8);
BigDecimal totalFees = atFees;
// This instance used for AT processing
this.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp,
BlockData preAtBlockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp,
minter.getPublicKey(), minterSignature, atCount, atFees,
encodedOnlineAccounts, onlineAccountsCount, onlineAccountsTimestamp, onlineAccountsSignatures);
// Requires this.blockData and this.transactions, sets this.ourAtStates and this.ourAtFees
this.executeATs();
Block newBlock = new Block(repository, preAtBlockData, minter);
atCount = this.ourAtStates.size();
this.atStates = this.ourAtStates;
atFees = this.ourAtFees;
// Requires blockData and transactions, sets ourAtStates and ourAtFees
newBlock.executeATs();
atCount = newBlock.ourAtStates.size();
newBlock.atStates = newBlock.ourAtStates;
atFees = newBlock.ourAtFees;
totalFees = atFees;
// Rebuild blockData using post-AT-execute data
this.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp,
newBlock.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp,
minter.getPublicKey(), minterSignature, atCount, atFees,
encodedOnlineAccounts, onlineAccountsCount, onlineAccountsTimestamp, onlineAccountsSignatures);
return newBlock;
}
/**
* Construct another block using this block as template, but with different minting account.
* Mints new block using this block as template, but with different minting account.
* <p>
* NOTE: uses the same transactions list, AT states, etc.
*
@ -358,7 +374,7 @@ public class Block {
* @return
* @throws DataException
*/
public Block newMinter(PrivateKeyAccount minter) throws DataException {
public Block remint(PrivateKeyAccount minter) throws DataException {
Block newBlock = new Block(this.repository, this.blockData);
newBlock.minter = minter;
@ -380,7 +396,12 @@ public class Block {
throw new DataException("Unable to calculate next block's minter signature", e);
}
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey());
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey());
if (minterLevel == 0)
throw new IllegalStateException("Minter effective level returned zero?");
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
newBlock.transactions = this.transactions;
int transactionCount = this.blockData.getTransactionCount();
@ -714,15 +735,15 @@ public class Block {
return Crypto.digest(Bytes.concat(Longs.toByteArray(height), publicKey));
}
public static BigInteger calcKeyDistance(int parentHeight, byte[] parentBlockSignature, byte[] publicKey) {
public static BigInteger calcKeyDistance(int parentHeight, byte[] parentBlockSignature, byte[] publicKey, int accountLevel) {
byte[] idealKey = calcIdealMinterPublicKey(parentHeight, parentBlockSignature);
byte[] perturbedKey = calcHeightPerturbedPublicKey(parentHeight + 1, publicKey);
return MAX_DISTANCE.subtract(new BigInteger(idealKey).subtract(new BigInteger(perturbedKey)).abs());
return MAX_DISTANCE.subtract(new BigInteger(idealKey).subtract(new BigInteger(perturbedKey)).abs()).divide(BigInteger.valueOf(accountLevel));
}
public static BigInteger calcBlockWeight(int parentHeight, byte[] parentBlockSignature, BlockSummaryData blockSummaryData) {
BigInteger keyDistance = calcKeyDistance(parentHeight, parentBlockSignature, blockSummaryData.getMinterPublicKey());
BigInteger keyDistance = calcKeyDistance(parentHeight, parentBlockSignature, blockSummaryData.getMinterPublicKey(), blockSummaryData.getMinterLevel());
return BigInteger.valueOf(blockSummaryData.getOnlineAccountsCount()).shiftLeft(ACCOUNTS_COUNT_SHIFT).add(keyDistance);
}
@ -753,8 +774,8 @@ public class Block {
* 20% of (90s - 30s) is 12s<br>
* So this block's timestamp is previous block's timestamp + 30s + 12s.
*/
public static long calcTimestamp(BlockData parentBlockData, byte[] minterPublicKey) {
BigInteger distance = calcKeyDistance(parentBlockData.getHeight(), parentBlockData.getSignature(), minterPublicKey);
public static long calcTimestamp(BlockData parentBlockData, byte[] minterPublicKey, int minterAccountLevel) {
BigInteger distance = calcKeyDistance(parentBlockData.getHeight(), parentBlockData.getSignature(), minterPublicKey, minterAccountLevel);
final int thisHeight = parentBlockData.getHeight() + 1;
BlockTimingByHeight blockTiming = BlockChain.getInstance().getBlockTimingByHeight(thisHeight);
@ -837,7 +858,12 @@ public class Block {
if (this.blockData.getTimestamp() < Block.calcMinimumTimestamp(parentBlockData))
return ValidationResult.TIMESTAMP_TOO_SOON;
long expectedTimestamp = calcTimestamp(parentBlockData, this.blockData.getMinterPublicKey());
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, this.blockData.getMinterPublicKey());
if (minterLevel == 0)
return ValidationResult.MINTER_NOT_ACCEPTED;
long expectedTimestamp = calcTimestamp(parentBlockData, this.blockData.getMinterPublicKey(), minterLevel);
if (this.blockData.getTimestamp() != expectedTimestamp)
return ValidationResult.TIMESTAMP_INCORRECT;
@ -1112,7 +1138,7 @@ public class Block {
throw new IllegalStateException("Attempted to execute ATs when block's local AT state data already exists");
// AT-Transactions generated by running ATs, to be prepended to block's transactions
List<AtTransaction> allATTransactions = new ArrayList<>();
List<AtTransaction> allAtTransactions = new ArrayList<>();
this.ourAtStates = new ArrayList<>();
this.ourAtFees = BigDecimal.ZERO.setScale(8);
@ -1125,7 +1151,7 @@ public class Block {
AT at = new AT(this.repository, atData);
List<AtTransaction> atTransactions = at.run(this.blockData.getTimestamp());
allATTransactions.addAll(atTransactions);
allAtTransactions.addAll(atTransactions);
ATStateData atStateData = at.getATStateData();
this.ourAtStates.add(atStateData);
@ -1134,7 +1160,7 @@ public class Block {
}
// Prepend our entire AT-Transactions/states to block's transactions
this.transactions.addAll(0, allATTransactions);
this.transactions.addAll(0, allAtTransactions);
// Re-sort
this.transactions.sort(Transaction.getComparator());

View File

@ -149,6 +149,7 @@ public class BlockChain {
private int minAccountLevelToMint = 1;
private int minAccountLevelToRewardShare;
private int maxRewardSharesPerMintingAccount;
private int founderEffectiveMintingLevel;
/** Minimum time to retain online account signatures (ms) for block validity checks. */
private long onlineAccountSignaturesMinLifetime;
@ -330,6 +331,10 @@ public class BlockChain {
return this.maxRewardSharesPerMintingAccount;
}
public int getFounderEffectiveMintingLevel() {
return this.founderEffectiveMintingLevel;
}
public long getOnlineAccountSignaturesMinLifetime() {
return this.onlineAccountSignaturesMinLifetime;
}
@ -430,6 +435,8 @@ public class BlockChain {
if (this.minAccountLevelToRewardShare <= 0)
Settings.throwValidationError("Invalid/missing \"minAccountLevelToRewardShare\" in blockchain config");
if (this.founderEffectiveMintingLevel <= 0)
Settings.throwValidationError("Invalid/missing \"founderEffectiveMintingLevel\" in blockchain config");
if (this.featureTriggers == null)
Settings.throwValidationError("No \"featureTriggers\" entry found in blockchain config");

View File

@ -145,12 +145,12 @@ public class BlockMinter extends Thread {
for (PrivateKeyAccount mintingAccount : mintingAccounts) {
// First block does the AT heavy-lifting
if (newBlocks.isEmpty()) {
Block newBlock = new Block(repository, previousBlock.getBlockData(), mintingAccount);
Block newBlock = Block.mint(repository, previousBlock.getBlockData(), mintingAccount);
newBlocks.add(newBlock);
} else {
// The blocks for other minters require less effort...
Block newBlock = newBlocks.get(0);
newBlocks.add(newBlock.newMinter(mintingAccount));
newBlocks.add(newBlock.remint(mintingAccount));
}
}
@ -338,7 +338,7 @@ public class BlockMinter extends Thread {
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();
Block newBlock = new Block(repository, previousBlockData, mintingAccount);
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
// Make sure we're the only thread modifying the blockchain
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();

View File

@ -10,8 +10,11 @@ import java.util.stream.Collectors;
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.Block;
import org.qora.block.Block.ValidationResult;
import org.qora.data.account.RewardShareData;
import org.qora.data.block.BlockData;
import org.qora.data.block.BlockSummaryData;
import org.qora.data.network.PeerChainTipData;
@ -187,6 +190,10 @@ public class Synchronizer {
// Fetch our corresponding block summaries
List<BlockSummaryData> ourBlockSummaries = repository.getBlockRepository().getBlockSummaries(commonBlockHeight + 1, ourInitialHeight);
// Populate minter account levels for both lists of block summaries
populateBlockSummariesMinterLevels(repository, peerBlockSummaries);
populateBlockSummariesMinterLevels(repository, ourBlockSummaries);
// Calculate cumulative chain weights of both blockchain subsets, from common block to highest mutual block.
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries);
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries);
@ -418,11 +425,23 @@ public class Synchronizer {
BlockMessage blockMessage = (BlockMessage) message;
try {
return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStates());
} catch (DataException e) {
LOGGER.debug("Failed to create block", e);
return null;
return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStates());
}
private void populateBlockSummariesMinterLevels(Repository repository, List<BlockSummaryData> blockSummaries) throws DataException {
for (int i = 0; i < blockSummaries.size(); ++i) {
BlockSummaryData blockSummary = blockSummaries.get(i);
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockSummary.getMinterPublicKey());
if (minterLevel == 0) {
// We don't want to throw, or use zero, as this will kill Controller thread and make client unstable.
// So we log this but use 1 instead
LOGGER.warn(String.format("Unexpected zero effective minter level for reward-share %s - using 1 instead!", Base58.encode(blockSummary.getMinterPublicKey())));
minterLevel = 1;
}
blockSummary.setMinterLevel(minterLevel);
}
}

View File

@ -10,6 +10,9 @@ public class BlockSummaryData {
private byte[] minterPublicKey;
private int onlineAccountsCount;
// Optional, set after construction
private Integer minterLevel;
// Constructors
public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, int onlineAccountsCount) {
this.height = height;
@ -49,4 +52,12 @@ public class BlockSummaryData {
return this.onlineAccountsCount;
}
public Integer getMinterLevel() {
return this.minterLevel;
}
public void setMinterLevel(Integer minterLevel) {
this.minterLevel = minterLevel;
}
}

View File

@ -10,6 +10,7 @@
"oneNamePerAccount": true,
"minAccountLevelToRewardShare": 5,
"maxRewardSharesPerMintingAccount": 20,
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 2592000000,
"onlineAccountSignaturesMaxLifetime": 3196800000,
"rewardsByHeight": [
@ -32,6 +33,7 @@
{ "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
"blocksNeededByLevel": [ 7200, 64800, 129600, 172800, 244000, 345600, 518400, 691200, 864000, 1036800 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
@ -56,26 +58,29 @@
"transactions": [
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT", "description": "QORTAL coin", "quantity": 0, "isDivisible": true, "reference": "28u54WRcMfGujtQMZ9dNKFXVqucY7XfPihXAqPFsnx853NPUwfDJy1sMH5boCkahFgjUNYqc5fkduxdBhQTKgUsC", "data": "{}" },
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "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": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "amount": "1000" },
{ "type": "ACCOUNT_FLAGS", "target": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_LEVEL", "target": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "level": 8 },
{ "type": "REWARD_SHARE", "minterPublicKey": "6rNn9b3pYRrG9UKqzMWYZ9qa8F3Zgv2mVWrULGHUusb", "recipient": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "rewardSharePublicKey": "8X3w1521UNnnonieugAxhfbfvqoRpwPXJrwGQZb5JjQ3", "sharePercent": 100 },
{ "type": "GENESIS", "recipient": "QcatoCyyp7dVfMtJ92sgUUPDoBJevaemRX", "amount": "1000" },
{ "type": "ACCOUNT_FLAGS", "target": "QcatoCyyp7dVfMtJ92sgUUPDoBJevaemRX", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_LEVEL", "target": "QcatoCyyp7dVfMtJ92sgUUPDoBJevaemRX", "level": 3 },
{ "type": "GENESIS", "recipient": "QTiga19sttbf6CLQLT83mhCSWEaCvjk8th", "amount": "1000" },
{ "type": "ACCOUNT_FLAGS", "target": "QTiga19sttbf6CLQLT83mhCSWEaCvjk8th", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_LEVEL", "target": "QTiga19sttbf6CLQLT83mhCSWEaCvjk8th", "level": 10 },
{ "type": "GENESIS", "recipient": "QcrowX39FuycKvMFFBsakyd5HSxe7bxFsn", "amount": "1000" },
{ "type": "ACCOUNT_FLAGS", "target": "QcrowX39FuycKvMFFBsakyd5HSxe7bxFsn", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_LEVEL", "target": "QcrowX39FuycKvMFFBsakyd5HSxe7bxFsn", "level": 10 },
{ "type": "CREATE_GROUP", "creatorPublicKey": "6rNn9b3pYRrG9UKqzMWYZ9qa8F3Zgv2mVWrULGHUusb", "owner": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT60", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
{ "type": "GENESIS", "recipient": "QMtx2UmUuRZckCmRJRyxdzSAazHP8hU5rA", "amount": "160672815.43629771", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMkgf9Y6Ac2TUrynDvyhX69ekpC3P3GQmN", "amount": "99008835.47860426", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QWZm17rRXeUehcM4TprVNNRSTHWQmG2bME", "amount": "62663714.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "Qa95hURaNK4kPhDhbdmDFm2wMkkoWFZ4Zz", "amount": "40976709.97984710", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPEoMF2dA7NHrHhsSG9zczCFwx9wFdWvzT", "amount": "10033147.61257500", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QjJjjBUJSZAMuYiwTyfJTFthH6SrofjG6d", "amount": "8871800.22712502", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QRj4VNEthakckhYpCJMEBhEFk12pa7GPJT", "amount": "7810001.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMNdPz11XubtvxXLGeiG3PHKaQW67LkZMp", "amount": "4056950.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QhUWSWWFt6vDy2qNFn68JPTPLjyDrzrh4D", "amount": "3220564.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMrECKgkohx6ZXEMdLzikqBmAkdyHeQDqL", "amount": "997498.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QMQAG28pyv2aZVjWbKoRn39Ytir6rLPnTK", "amount": "100745.00000000", "assetId": 1 },
{ "type": "GENESIS", "recipient": "QPZTxWtCmH6Y6zwwntjnPDfKG6zNKRivqJ", "amount": "1282.61375000", "assetId": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QQKeokRiFCgAhBSdu1DUf5e1LCkgApvrxZ", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QiaaoNZ54wKoaUMXxW72UsPt1MiPpeUTWm", "level": 5 },
{ "type": "ACCOUNT_LEVEL", "target": "QN5XF1YQUyVt3S1LNZtStXQCbtxyhkj2FR", "level": 5 },

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
}