From 9b859f3efd894aa09bf5133633eb30e3cbac5339 Mon Sep 17 00:00:00 2001 From: catbref Date: Mon, 11 Mar 2019 07:45:14 +0000 Subject: [PATCH] Interim minting commit with block rewards (untested) + minting long-term simulator + Maven pom.xml changes for Eclipse IDE --- src/main/java/org/qora/block/Block.java | 79 +++++++- src/main/java/org/qora/block/BlockChain.java | 12 ++ src/main/java/org/qora/mintsim.java | 182 +++++++++++++++++++ 3 files changed, 264 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/qora/mintsim.java diff --git a/src/main/java/org/qora/block/Block.java b/src/main/java/org/qora/block/Block.java index ada5cfc5..0b33f5c9 100644 --- a/src/main/java/org/qora/block/Block.java +++ b/src/main/java/org/qora/block/Block.java @@ -18,7 +18,9 @@ import org.qora.account.PrivateKeyAccount; import org.qora.account.PublicKeyAccount; import org.qora.asset.Asset; import org.qora.at.AT; +import org.qora.block.BlockChain.RewardsByHeight; import org.qora.crypto.Crypto; +import org.qora.data.account.ProxyForgerData; import org.qora.data.at.ATData; import org.qora.data.at.ATStateData; import org.qora.data.block.BlockData; @@ -962,13 +964,20 @@ public class Block { return true; } - + /** * Process block, and its transactions, adding them to the blockchain. * * @throws DataException */ public void process() throws DataException { + // Set our block's height + int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight(); + this.blockData.setHeight(blockchainHeight + 1); + + // Block rewards go before transactions processed + processBlockRewards(); + // Process transactions (we'll link them to this block after saving the block itself) // AT-generated transactions are already added to our transactions so no special handling is needed here. List transactions = this.getTransactions(); @@ -980,9 +989,6 @@ public class Block { if (blockFee.compareTo(BigDecimal.ZERO) > 0) this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).add(blockFee)); - // Block rewards go here - processBlockRewards(); - // Process AT fees and save AT states into repository ATRepository atRepository = this.repository.getATRepository(); for (ATStateData atState : this.getATStates()) { @@ -995,12 +1001,10 @@ public class Block { } // Link block into blockchain by fetching signature of highest block and setting that as our reference - int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight(); BlockData latestBlockData = this.repository.getBlockRepository().fromHeight(blockchainHeight); if (latestBlockData != null) this.blockData.setReference(latestBlockData.getSignature()); - this.blockData.setHeight(blockchainHeight + 1); this.repository.getBlockRepository().save(this.blockData); // Link transactions to this block, thus removing them from unconfirmed transactions list. @@ -1023,7 +1027,28 @@ public class Block { } protected void processBlockRewards() throws DataException { - // NOP for vanilla qora-core + BigDecimal reward = getRewardAtHeight(this.blockData.getHeight()); + + // No reward for our height? + if (reward == null) + return; + + // Is generator public key actually a proxy forge key? + ProxyForgerData proxyForgerData = this.repository.getAccountRepository().getProxyForgeData(this.blockData.getGeneratorPublicKey()); + if (proxyForgerData != null) { + // Split reward to forger and recipient; + Account recipient = new Account(this.repository, proxyForgerData.getRecipient()); + BigDecimal recipientShare = reward.multiply(proxyForgerData.getShare()); + recipient.setConfirmedBalance(Asset.QORA, recipient.getConfirmedBalance(Asset.QORA).add(recipientShare)); + + Account forger = new PublicKeyAccount(this.repository, proxyForgerData.getForgerPublicKey()); + BigDecimal forgerShare = reward.subtract(recipientShare); + forger.setConfirmedBalance(Asset.QORA, forger.getConfirmedBalance(Asset.QORA).add(forgerShare)); + return; + } + + // Give block reward to generator + this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).add(reward)); } /** @@ -1051,7 +1076,7 @@ public class Block { this.repository.getTransactionRepository().deleteParticipants(transaction.getTransactionData()); } - // Block rewards removed here + // Block rewards removed after transactions undone orphanBlockRewards(); // If fees are non-zero then remove fees from generator's balance @@ -1075,7 +1100,43 @@ public class Block { } protected void orphanBlockRewards() throws DataException { - // NOP for vanilla qora-core + BigDecimal reward = getRewardAtHeight(this.blockData.getHeight()); + + // No reward for our height? + if (reward == null) + return; + + // Is generator public key actually a proxy forge key? + ProxyForgerData proxyForgerData = this.repository.getAccountRepository().getProxyForgeData(this.blockData.getGeneratorPublicKey()); + if (proxyForgerData != null) { + // Split reward from forger and recipient; + Account recipient = new Account(this.repository, proxyForgerData.getRecipient()); + BigDecimal recipientShare = reward.multiply(proxyForgerData.getShare()); + recipient.setConfirmedBalance(Asset.QORA, recipient.getConfirmedBalance(Asset.QORA).subtract(recipientShare)); + + Account forger = new PublicKeyAccount(this.repository, proxyForgerData.getForgerPublicKey()); + BigDecimal forgerShare = reward.subtract(recipientShare); + forger.setConfirmedBalance(Asset.QORA, forger.getConfirmedBalance(Asset.QORA).subtract(forgerShare)); + return; + } + + // Take block reward from generator + this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).subtract(reward)); + } + + protected BigDecimal getRewardAtHeight(int ourHeight) { + List rewardsByHeight = BlockChain.getInstance().getBlockRewardsByHeight(); + + // No rewards configured? + if (rewardsByHeight == null) + return null; + + // Scan through for reward at our height + for (RewardsByHeight rewardInfo : rewardsByHeight) + if (rewardInfo.height <= ourHeight) + return rewardInfo.reward; + + return null; } /** diff --git a/src/main/java/org/qora/block/BlockChain.java b/src/main/java/org/qora/block/BlockChain.java index a858b07a..258b644b 100644 --- a/src/main/java/org/qora/block/BlockChain.java +++ b/src/main/java/org/qora/block/BlockChain.java @@ -7,6 +7,7 @@ import java.io.Reader; import java.math.BigDecimal; import java.math.MathContext; import java.sql.SQLException; +import java.util.List; import java.util.Map; import javax.xml.bind.JAXBContext; @@ -88,6 +89,13 @@ public class BlockChain { /** Whether only one registered name is allowed per account. */ private boolean oneNamePerAccount = false; + /** Block rewards by block height */ + public static class RewardsByHeight { + public int height; + public BigDecimal reward; + } + List rewardsByHeight; + // Constructors, etc. private BlockChain() { @@ -222,6 +230,10 @@ public class BlockChain { return this.oneNamePerAccount; } + public List getBlockRewardsByHeight() { + return this.rewardsByHeight; + } + // Convenience methods for specific blockchain feature triggers public long getMessageReleaseHeight() { diff --git a/src/main/java/org/qora/mintsim.java b/src/main/java/org/qora/mintsim.java new file mode 100644 index 00000000..c179dba2 --- /dev/null +++ b/src/main/java/org/qora/mintsim.java @@ -0,0 +1,182 @@ +package org.qora; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class mintsim { + + private static final int NUMBER_BLOCKS = 5_000_000; + private static final double GRANT_PROB = 0.001; + private static final int BLOCK_HISTORY = 0; + private static final int WEIGHTING = 2; + + private static final int TOP_MINTERS_SIZE = 200; + + private static final Random random = new Random(); + private static List tiers = new ArrayList<>(); + private static List accounts = new ArrayList<>(); + private static List blockMinters = new ArrayList<>(); + + private static List accountsWithGrants = new ArrayList<>(); + + public static class TierInfo { + public final int maxAccounts; + public final int minBlocks; + public int numberAccounts; + + public TierInfo(int maxAccounts, int minBlocks) { + this.maxAccounts = maxAccounts; + this.minBlocks = minBlocks; + this.numberAccounts = 0; + } + } + + public static class Account { + public final int tierIndex; + public int blocksForged; + public int rightsGranted; + + public Account(int tierIndex) { + this.tierIndex = tierIndex; + this.blocksForged = 0; + this.rightsGranted = 0; + } + } + + public static void main(String args[]) { + if (args.length < 2 || (args.length % 2) != 0) { + System.err.println("usage: mintsim [ [...]]"); + System.exit(1); + } + + try { + int argIndex = 0; + do { + int minBlocks = Integer.valueOf(args[argIndex++]); + int maxAccounts = Integer.valueOf(args[argIndex++]); + + tiers.add(new TierInfo(maxAccounts, minBlocks)); + } while (argIndex < args.length); + } catch (NumberFormatException e) { + System.err.println("Can't parse number?"); + System.exit(2); + } + + // Print summary + System.out.println(String.format("Number of tiers: %d", tiers.size())); + + for (int i = 0; i < tiers.size(); ++i) { + TierInfo tier = tiers.get(i); + System.out.println(String.format("Tier %d:", i)); + System.out.println(String.format("\tMinimum forged blocks to grant right: %d", tier.minBlocks)); + System.out.println(String.format("\tMaximum tier%d grants: %d", i + 1, tier.maxAccounts)); + } + + TierInfo initialTier = tiers.get(0); + + int totalAccounts = initialTier.maxAccounts; + for (int i = 1; i < tiers.size(); ++i) + totalAccounts *= 1 + tiers.get(i).maxAccounts; + + System.out.println(String.format("Total accounts: %d", totalAccounts)); + + // Create initial accounts + initialTier.numberAccounts = initialTier.maxAccounts; + for (int i = 0; i < initialTier.maxAccounts; ++i) + accounts.add(new Account(0)); + + for (int height = 1; height < NUMBER_BLOCKS; ++height) { + int minterId = pickMinterId(); + Account minter = accounts.get(minterId); + + ++minter.blocksForged; + blockMinters.add(minterId); + + if (minter.tierIndex < tiers.size() - 1) { + TierInfo nextTier = tiers.get(minter.tierIndex + 1); + + // Minter just reached threshold to grant rights + if (minter.blocksForged == nextTier.minBlocks) + accountsWithGrants.add(minterId); + } + + List accountsToRemove = new ArrayList<>(); + // Do any account with spare grants want to grant? + for (int granterId : accountsWithGrants) { + if (random.nextDouble() >= GRANT_PROB) + continue; + + Account granter = accounts.get(granterId); + TierInfo nextTier = tiers.get(granter.tierIndex + 1); + + accounts.add(new Account(granter.tierIndex + 1)); + + ++nextTier.numberAccounts; + ++granter.rightsGranted; + + if (granter.rightsGranted == nextTier.maxAccounts) + accountsToRemove.add(granterId); + } + + // Remove granters that have used their allowance + accountsWithGrants.removeAll(accountsToRemove); + + if (height % 100000 == 0) { + System.out.println(String.format("Summary after block %d:", height)); + for (int i = 0; i < tiers.size(); ++i) + System.out.println(String.format("\tTier %d: number of accounts: %d", i, tiers.get(i).numberAccounts)); + } + } + + // Top minters + List topMinters = new ArrayList<>(); + for (int i = 0; i < accounts.size(); ++i) { + topMinters.add(i); + topMinters.sort((a, b) -> Integer.compare(accounts.get(b).blocksForged, accounts.get(a).blocksForged)); + + if (topMinters.size() > TOP_MINTERS_SIZE) + topMinters.remove(TOP_MINTERS_SIZE); + } + + System.out.println(String.format("Top %d minters:", TOP_MINTERS_SIZE)); + for (int i = 0; i < topMinters.size(); ++i) { + int topMinterId = topMinters.get(i); + Account topMinter = accounts.get(topMinterId); + System.out.println(String.format("\tAccount %d (tier %d) has minted %d blocks", topMinterId, topMinter.tierIndex, topMinter.blocksForged)); + } + + for (int i = 0; i < tiers.size(); ++i) + System.out.println(String.format("Tier %d: number of accounts: %d", i, tiers.get(i).numberAccounts)); + } + + private static int pickMinterId() { + // There might not be enough block history yet... + final int blockHistory = Math.min(BLOCK_HISTORY, blockMinters.size()); + + // Weighting (W) + + // An account that HASN'T forged in the last X blocks has 1 standard chance to forge + // but an account that HAS forged Y in the last X blocks has 1 + (Y / X) * W chances to forge + // e.g. forged 25 in last 100 blocks, with weighting 8, gives (25 / 100) * 8 = 2 extra chances + + // So in X blocks there will be X * W extra chances. + // We pick winning number from (number-of-accounts + X * W) chances + int totalChances = accounts.size() + blockHistory * WEIGHTING; + int winningNumber = random.nextInt(totalChances); + + // Simple case if winning number is less than number of accounts, + // otherwise we need to handle extra chances for accounts that have forged in last X blocks. + if (winningNumber < accounts.size()) + return winningNumber; + + // Handling extra chances + + // We can work out which block in last X blocks as each block is worth W chances + int blockOffset = (winningNumber - accounts.size()) / WEIGHTING; + int blockIndex = blockMinters.size() - 1 - blockOffset; + + return blockMinters.get(blockIndex); + } + +}