diff --git a/src/main/java/org/qora/RewardShareKeys.java b/src/main/java/org/qora/RewardShareKeys.java index 6a1afded..689323b9 100644 --- a/src/main/java/org/qora/RewardShareKeys.java +++ b/src/main/java/org/qora/RewardShareKeys.java @@ -11,26 +11,29 @@ import org.qora.utils.Base58; public class RewardShareKeys { private static void usage() { - System.err.println("Usage: RewardShareKeys "); + System.err.println("Usage: RewardShareKeys []"); 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))); diff --git a/src/main/java/org/qora/account/Account.java b/src/main/java/org/qora/account/Account.java index 72c3a6a9..e7739557 100644 --- a/src/main/java/org/qora/account/Account.java +++ b/src/main/java/org/qora/account/Account.java @@ -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. + *

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

+ * 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(); + } + } diff --git a/src/main/java/org/qora/block/Block.java b/src/main/java/org/qora/block/Block.java index 48b68109..423b4e28 100644 --- a/src/main/java/org/qora/block/Block.java +++ b/src/main/java/org/qora/block/Block.java @@ -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. *

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

* 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 transactions, List atStates) throws DataException { + public Block(Repository repository, BlockData blockData, List transactions, List 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. *

* This constructor typically used when minting a new block. *

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

* 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
* 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 allATTransactions = new ArrayList<>(); + List 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 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()); diff --git a/src/main/java/org/qora/block/BlockChain.java b/src/main/java/org/qora/block/BlockChain.java index 7aa39165..dba8afdd 100644 --- a/src/main/java/org/qora/block/BlockChain.java +++ b/src/main/java/org/qora/block/BlockChain.java @@ -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"); diff --git a/src/main/java/org/qora/block/BlockMinter.java b/src/main/java/org/qora/block/BlockMinter.java index 5abf30c0..493b6146 100644 --- a/src/main/java/org/qora/block/BlockMinter.java +++ b/src/main/java/org/qora/block/BlockMinter.java @@ -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(); diff --git a/src/main/java/org/qora/controller/Synchronizer.java b/src/main/java/org/qora/controller/Synchronizer.java index 6d7c261b..b104ac6b 100644 --- a/src/main/java/org/qora/controller/Synchronizer.java +++ b/src/main/java/org/qora/controller/Synchronizer.java @@ -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 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 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); } } diff --git a/src/main/java/org/qora/data/block/BlockSummaryData.java b/src/main/java/org/qora/data/block/BlockSummaryData.java index 327d2d4b..aaa945b9 100644 --- a/src/main/java/org/qora/data/block/BlockSummaryData.java +++ b/src/main/java/org/qora/data/block/BlockSummaryData.java @@ -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; + } + } diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index b04f6417..52754f1e 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -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 }, diff --git a/src/test/java/org/qora/test/ChainWeightTests.java b/src/test/java/org/qora/test/ChainWeightTests.java index 48016675..62dd225d 100644 --- a/src/test/java/org/qora/test/ChainWeightTests.java +++ b/src/test/java/org/qora/test/ChainWeightTests.java @@ -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 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 genBlockSummaries(int count, BlockSummaryData commonBlockSummary) { + private static List genBlockSummaries(Repository repository, int count, BlockSummaryData commonBlockSummary) { List 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 shorterChain = genBlockSummaries(3, commonBlockSummary); - List longerChain = genBlockSummaries(shorterChain.size() + 1, commonBlockSummary); + List shorterChain = genBlockSummaries(repository, 3, commonBlockSummary); + List 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 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); } } diff --git a/src/test/java/org/qora/test/TransactionTests.java b/src/test/java/org/qora/test/TransactionTests.java index 1e9b09f9..cb8acba9 100644 --- a/src/test/java/org/qora/test/TransactionTests.java +++ b/src/test/java/org/qora/test/TransactionTests.java @@ -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; diff --git a/src/test/java/org/qora/test/common/Common.java b/src/test/java/org/qora/test/common/Common.java index 198b763c..172bff69 100644 --- a/src/test/java/org/qora/test/common/Common.java +++ b/src/test/java/org/qora/test/common/Common.java @@ -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 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 getTestAccounts(Repository repository) { return testAccountsByName.values().stream().map(account -> new TestAccount(repository, account)).collect(Collectors.toList()); } diff --git a/src/test/resources/test-chain-v1.json b/src/test/resources/test-chain-v1.json index 13c9cc54..b93659d9 100644 --- a/src/test/resources/test-chain-v1.json +++ b/src/test/resources/test-chain-v1.json @@ -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 } diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json new file mode 100644 index 00000000..dda924a0 --- /dev/null +++ b/src/test/resources/test-chain-v2-minting.json @@ -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 } + ] + } +} diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index 4cee7dc2..fd230218 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -8,6 +8,7 @@ "requireGroupForApproval": false, "minAccountLevelToRewardShare": 5, "maxRewardSharesPerMintingAccount": 20, + "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "rewardsByHeight": [ diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 9b4f36bd..ee8e4315 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -8,6 +8,7 @@ "requireGroupForApproval": false, "minAccountLevelToRewardShare": 5, "maxRewardSharesPerMintingAccount": 20, + "founderEffectiveMintingLevel": 10, "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "rewardsByHeight": [ diff --git a/src/test/resources/test-settings-v2-minting.json b/src/test/resources/test-settings-v2-minting.json new file mode 100644 index 00000000..eba9a655 --- /dev/null +++ b/src/test/resources/test-settings-v2-minting.json @@ -0,0 +1,6 @@ +{ + "restrictedApi": false, + "blockchainConfig": "src/test/resources/test-chain-v2-minting.json", + "wipeUnconfirmedOnStart": false, + "minPeers": 0 +}