Merge branch 'master' into pirate-chain

This commit is contained in:
CalDescent
2022-07-31 16:00:52 +01:00
25 changed files with 827 additions and 126 deletions

View File

@@ -199,6 +199,11 @@ public class Block {
}
public boolean hasShareBin(AccountLevelShareBin shareBin, int blockHeight) {
AccountLevelShareBin ourShareBin = this.getShareBin(blockHeight);
return ourShareBin != null && shareBin.id == ourShareBin.id;
}
public long distribute(long accountAmount, Map<String, Long> balanceChanges) {
if (this.isRecipientAlsoMinter) {
// minter & recipient the same - simpler case
@@ -1891,12 +1896,67 @@ public class Block {
final boolean haveFounders = !onlineFounderAccounts.isEmpty();
// Determine reward candidates based on account level
List<AccountLevelShareBin> accountLevelShareBins = BlockChain.getInstance().getAccountLevelShareBins();
for (int binIndex = 0; binIndex < accountLevelShareBins.size(); ++binIndex) {
// Find all accounts in share bin. getShareBin() returns null for minter accounts that are also founders, so they are effectively filtered out.
// This needs a deep copy, so the shares can be modified when tiers aren't activated yet
List<AccountLevelShareBin> accountLevelShareBins = new ArrayList<>();
for (AccountLevelShareBin accountLevelShareBin : BlockChain.getInstance().getAccountLevelShareBins()) {
accountLevelShareBins.add((AccountLevelShareBin) accountLevelShareBin.clone());
}
Map<Integer, List<ExpandedAccount>> accountsForShareBin = new HashMap<>();
// We might need to combine some share bins if they haven't reached the minimum number of minters yet
for (int binIndex = accountLevelShareBins.size()-1; binIndex >= 0; --binIndex) {
AccountLevelShareBin accountLevelShareBin = accountLevelShareBins.get(binIndex);
// Object reference compare is OK as all references are read-only from blockchain config.
List<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.getShareBin(this.blockData.getHeight()) == accountLevelShareBin).collect(Collectors.toList());
// Find all accounts in share bin. getShareBin() returns null for minter accounts that are also founders, so they are effectively filtered out.
List<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.hasShareBin(accountLevelShareBin, this.blockData.getHeight())).collect(Collectors.toList());
// Add any accounts that have been moved down from a higher tier
List<ExpandedAccount> existingBinnedAccounts = accountsForShareBin.get(binIndex);
if (existingBinnedAccounts != null)
binnedAccounts.addAll(existingBinnedAccounts);
// Logic below may only apply to higher levels, and only for share bins with a specific range of online accounts
if (accountLevelShareBin.levels.get(0) < BlockChain.getInstance().getShareBinActivationMinLevel() ||
binnedAccounts.isEmpty() || binnedAccounts.size() >= BlockChain.getInstance().getMinAccountsToActivateShareBin()) {
// Add all accounts for this share bin to the accountsForShareBin list
accountsForShareBin.put(binIndex, binnedAccounts);
continue;
}
// Share bin contains more than one, but less than the minimum number of minters. We treat this share bin
// as not activated yet. In these cases, the rewards and minters are combined and paid out to the previous
// share bin, to prevent a single or handful of accounts receiving the entire rewards for a share bin.
//
// Example:
//
// - Share bin for levels 5 and 6 has 100 minters
// - Share bin for levels 7 and 8 has 10 minters
//
// This is below the minimum of 30, so share bins are reconstructed as follows:
//
// - Share bin for levels 5 and 6 now contains 110 minters
// - Share bin for levels 7 and 8 now contains 0 minters
// - Share bin for levels 5 and 6 now pays out rewards for levels 5, 6, 7, and 8
// - Share bin for levels 7 and 8 pays zero rewards
//
// This process is iterative, so will combine several tiers if needed.
// Designate this share bin as empty
accountsForShareBin.put(binIndex, new ArrayList<>());
// Move the accounts originally intended for this share bin to the previous one
accountsForShareBin.put(binIndex - 1, binnedAccounts);
// Move the block reward from this share bin to the previous one
AccountLevelShareBin previousShareBin = accountLevelShareBins.get(binIndex - 1);
previousShareBin.share += accountLevelShareBin.share;
accountLevelShareBin.share = 0L;
}
// Now loop through (potentially modified) share bins and determine the reward candidates
for (int binIndex = 0; binIndex < accountLevelShareBins.size(); ++binIndex) {
AccountLevelShareBin accountLevelShareBin = accountLevelShareBins.get(binIndex);
List<ExpandedAccount> binnedAccounts = accountsForShareBin.get(binIndex);
// No online accounts in this bin? Skip to next one
if (binnedAccounts.isEmpty())

View File

@@ -104,10 +104,23 @@ public class BlockChain {
private List<RewardByHeight> rewardsByHeight;
/** Share of block reward/fees by account level */
public static class AccountLevelShareBin {
public static class AccountLevelShareBin implements Cloneable {
public int id;
public List<Integer> levels;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long share;
public Object clone() {
AccountLevelShareBin shareBinCopy = new AccountLevelShareBin();
List<Integer> levelsCopy = new ArrayList<>();
for (Integer level : this.levels) {
levelsCopy.add(level);
}
shareBinCopy.id = this.id;
shareBinCopy.levels = levelsCopy;
shareBinCopy.share = this.share;
return shareBinCopy;
}
}
private List<AccountLevelShareBin> sharesByLevel;
/** Generated lookup of share-bin by account level */
@@ -121,6 +134,12 @@ public class BlockChain {
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private Long qoraPerQortReward;
/** Minimum number of accounts before a share bin is considered activated */
private int minAccountsToActivateShareBin;
/** Min level at which share bin activation takes place; lower levels allow less than minAccountsPerShareBin */
private int shareBinActivationMinLevel;
/**
* Number of minted blocks required to reach next level from previous.
* <p>
@@ -166,6 +185,10 @@ public class BlockChain {
/** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */
private long onlineAccountSignaturesMaxLifetime;
/** Feature trigger timestamp for ONLINE_ACCOUNTS_MODULUS time interval increase. Can't use
* featureTriggers because unit tests need to set this value via Reflection. */
private long onlineAccountsModulusV2Timestamp;
/** Max reward shares by block height */
public static class MaxRewardSharesByTimestamp {
public long timestamp;
@@ -321,6 +344,11 @@ public class BlockChain {
return this.maxBlockSize;
}
// Online accounts
public long getOnlineAccountsModulusV2Timestamp() {
return this.onlineAccountsModulusV2Timestamp;
}
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
public boolean getRequireGroupForApproval() {
return this.requireGroupForApproval;
@@ -362,6 +390,14 @@ public class BlockChain {
return this.qoraPerQortReward;
}
public int getMinAccountsToActivateShareBin() {
return this.minAccountsToActivateShareBin;
}
public int getShareBinActivationMinLevel() {
return this.shareBinActivationMinLevel;
}
public int getMinAccountLevelToMint() {
return this.minAccountLevelToMint;
}

View File

@@ -90,37 +90,40 @@ public class BlockMinter extends Thread {
List<Block> newBlocks = new ArrayList<>();
// Flags for tracking change in whether minting is possible,
// so we can notify Controller, and further update SysTray, etc.
boolean isMintingPossible = false;
boolean wasMintingPossible = isMintingPossible;
while (running) {
if (isMintingPossible != wasMintingPossible)
Controller.getInstance().onMintingPossibleChange(isMintingPossible);
try (final Repository repository = RepositoryManager.getRepository()) {
// Going to need this a lot...
BlockRepository blockRepository = repository.getBlockRepository();
wasMintingPossible = isMintingPossible;
// Flags for tracking change in whether minting is possible,
// so we can notify Controller, and further update SysTray, etc.
boolean isMintingPossible = false;
boolean wasMintingPossible = isMintingPossible;
while (running) {
if (isMintingPossible != wasMintingPossible)
Controller.getInstance().onMintingPossibleChange(isMintingPossible);
try {
// Sleep for a while
Thread.sleep(1000);
wasMintingPossible = isMintingPossible;
isMintingPossible = false;
try {
// Free up any repository locks
repository.discardChanges();
final Long now = NTP.getTime();
if (now == null)
continue;
// Sleep for a while
Thread.sleep(1000);
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
if (minLatestBlockTimestamp == null)
continue;
isMintingPossible = false;
// No online accounts for current timestamp? (e.g. during startup)
if (!OnlineAccountsManager.getInstance().hasOnlineAccounts())
continue;
final Long now = NTP.getTime();
if (now == null)
continue;
try (final Repository repository = RepositoryManager.getRepository()) {
// Going to need this a lot...
BlockRepository blockRepository = repository.getBlockRepository();
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
if (minLatestBlockTimestamp == null)
continue;
// No online accounts for current timestamp? (e.g. during startup)
if (!OnlineAccountsManager.getInstance().hasOnlineAccounts())
continue;
List<MintingAccountData> mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
// No minting accounts?
@@ -198,10 +201,6 @@ public class BlockMinter extends Thread {
// so go ahead and mint a block if possible.
isMintingPossible = true;
// Reattach newBlocks to new repository handle
for (Block newBlock : newBlocks)
newBlock.setRepository(repository);
// Check blockchain hasn't changed
if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) {
previousBlockData = lastBlockData;
@@ -439,13 +438,13 @@ public class BlockMinter extends Thread {
Network network = Network.getInstance();
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
}
} catch (DataException e) {
LOGGER.warn("Repository issue while running block minter", e);
} catch (InterruptedException e) {
// We've been interrupted - time to exit
return;
}
} catch (InterruptedException e) {
// We've been interrupted - time to exit
return;
}
} catch (DataException e) {
LOGGER.warn("Repository issue while running block minter - NO LONGER MINTING", e);
}
}

View File

@@ -36,7 +36,8 @@ public class OnlineAccountsManager {
/**
* How long online accounts signatures last before they expire.
*/
public static final long ONLINE_TIMESTAMP_MODULUS = 5 * 60 * 1000L;
private static final long ONLINE_TIMESTAMP_MODULUS_V1 = 5 * 60 * 1000L;
private static final long ONLINE_TIMESTAMP_MODULUS_V2 = 30 * 60 * 1000L;
/**
* How many 'current' timestamp-sets of online accounts we cache.
@@ -78,12 +79,20 @@ public class OnlineAccountsManager {
private boolean hasOurOnlineAccounts = false;
public static long getOnlineTimestampModulus() {
Long now = NTP.getTime();
if (now != null && now >= BlockChain.getInstance().getOnlineAccountsModulusV2Timestamp()) {
return ONLINE_TIMESTAMP_MODULUS_V2;
}
return ONLINE_TIMESTAMP_MODULUS_V1;
}
public static Long getCurrentOnlineAccountTimestamp() {
Long now = NTP.getTime();
if (now == null)
return null;
return (now / ONLINE_TIMESTAMP_MODULUS) * ONLINE_TIMESTAMP_MODULUS;
long onlineTimestampModulus = getOnlineTimestampModulus();
return (now / onlineTimestampModulus) * onlineTimestampModulus;
}
private OnlineAccountsManager() {
@@ -203,11 +212,17 @@ public class OnlineAccountsManager {
long onlineAccountTimestamp = onlineAccountData.getTimestamp();
// Check timestamp is 'recent' here
if (Math.abs(onlineAccountTimestamp - now) > ONLINE_TIMESTAMP_MODULUS * 2) {
if (Math.abs(onlineAccountTimestamp - now) > getOnlineTimestampModulus() * 2) {
LOGGER.trace(() -> String.format("Rejecting online account %s with out of range timestamp %d", Base58.encode(rewardSharePublicKey), onlineAccountTimestamp));
return false;
}
// Check timestamp is a multiple of online timestamp modulus
if (onlineAccountTimestamp % getOnlineTimestampModulus() != 0) {
LOGGER.trace(() -> String.format("Rejecting online account %s with invalid timestamp %d", Base58.encode(rewardSharePublicKey), onlineAccountTimestamp));
return false;
}
// Verify signature
byte[] data = Longs.toByteArray(onlineAccountData.getTimestamp());
boolean isSignatureValid = onlineAccountTimestamp >= BlockChain.getInstance().getAggregateSignatureTimestamp()
@@ -308,7 +323,7 @@ public class OnlineAccountsManager {
if (now == null)
return;
final long cutoffThreshold = now - MAX_CACHED_TIMESTAMP_SETS * ONLINE_TIMESTAMP_MODULUS;
final long cutoffThreshold = now - MAX_CACHED_TIMESTAMP_SETS * getOnlineTimestampModulus();
this.currentOnlineAccounts.keySet().removeIf(timestamp -> timestamp < cutoffThreshold);
this.currentOnlineAccountsHashes.keySet().removeIf(timestamp -> timestamp < cutoffThreshold);
}

View File

@@ -29,6 +29,7 @@ import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import org.qortal.api.model.SimpleForeignTransaction;
import org.qortal.crypto.Crypto;
import org.qortal.settings.Settings;
import org.qortal.utils.Amounts;
import org.qortal.utils.BitTwiddling;
@@ -61,11 +62,6 @@ public abstract class Bitcoiny implements ForeignBlockchain {
/** How many wallet keys to generate in each batch. */
private static final int WALLET_KEY_LOOKAHEAD_INCREMENT = 3;
/** How many wallet keys to generate when using bitcoinj as the data provider.
* We must use a higher value here since we are unable to request multiple batches of keys.
* Without this, the bitcoinj state can be missing transactions, causing errors such as "insufficient balance". */
private static final int WALLET_KEY_LOOKAHEAD_INCREMENT_BITCOINJ = 50;
/** Byte offset into raw block headers to block timestamp. */
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
@@ -412,9 +408,6 @@ public abstract class Bitcoiny implements ForeignBlockchain {
Set<BitcoinyTransaction> walletTransactions = new HashSet<>();
Set<String> keySet = new HashSet<>();
// Set the number of consecutive empty batches required before giving up
final int numberOfAdditionalBatchesToSearch = 7;
int unusedCounter = 0;
int ki = 0;
do {
@@ -441,12 +434,12 @@ public abstract class Bitcoiny implements ForeignBlockchain {
if (areAllKeysUnused) {
// No transactions
if (unusedCounter >= numberOfAdditionalBatchesToSearch) {
if (unusedCounter >= Settings.getInstance().getGapLimit()) {
// ... and we've hit our search limit
break;
}
// We haven't hit our search limit yet so increment the counter and keep looking
unusedCounter++;
unusedCounter += WALLET_KEY_LOOKAHEAD_INCREMENT;
} else {
// Some keys in this batch were used, so reset the counter
unusedCounter = 0;
@@ -633,7 +626,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
this.keyChain = this.wallet.getActiveKeyChain();
// Set up wallet's key chain
this.keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT_BITCOINJ);
this.keyChain.setLookaheadSize(Settings.getInstance().getBitcoinjLookaheadSize());
this.keyChain.maybeLookAhead();
}

View File

@@ -289,6 +289,16 @@ public class Settings {
private Long testNtpOffset = null;
/* Foreign chains */
/** The number of consecutive empty addresses required before treating a wallet's transaction set as complete */
private int gapLimit = 24;
/** How many wallet keys to generate when using bitcoinj as the blockchain interface (e.g. when sending coins) */
private int bitcoinjLookaheadSize = 50;
// Data storage (QDN)
/** Data storage enabled/disabled*/
@@ -885,6 +895,15 @@ public class Settings {
}
public int getGapLimit() {
return this.gapLimit;
}
public int getBitcoinjLookaheadSize() {
return bitcoinjLookaheadSize;
}
public boolean isQdnEnabled() {
return this.qdnEnabled;
}

View File

@@ -12,7 +12,6 @@ import java.util.function.Supplier;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.Account;
import org.qortal.controller.Controller;
import org.qortal.controller.OnlineAccountsManager;
import org.qortal.controller.tradebot.TradeBot;
import org.qortal.crosschain.ACCT;
@@ -49,7 +48,7 @@ public class PresenceTransaction extends Transaction {
REWARD_SHARE(0) {
@Override
public long getLifetime() {
return OnlineAccountsManager.ONLINE_TIMESTAMP_MODULUS;
return OnlineAccountsManager.getOnlineTimestampModulus();
}
},
TRADE_BOT(1) {

View File

@@ -23,6 +23,7 @@
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 43200000,
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999,
"rewardsByHeight": [
{ "height": 1, "reward": 5.00 },
{ "height": 259201, "reward": 4.75 },
@@ -39,14 +40,16 @@
{ "height": 3110401, "reward": 2.00 }
],
"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 }
{ "id": 1, "levels": [ 1, 2 ], "share": 0.05 },
{ "id": 2, "levels": [ 3, 4 ], "share": 0.10 },
{ "id": 3, "levels": [ 5, 6 ], "share": 0.15 },
{ "id": 4, "levels": [ 7, 8 ], "share": 0.20 },
{ "id": 5, "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
"minAccountsToActivateShareBin": 30,
"shareBinActivationMinLevel": 7,
"blocksNeededByLevel": [ 7200, 64800, 129600, 172800, 244000, 345600, 518400, 691200, 864000, 1036800 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }