Chain history cleanups to correct legacy balance bugs.

This commit is contained in:
CalDescent 2023-07-14 13:49:26 +01:00
parent f808e80045
commit cd792fff55
5 changed files with 8169 additions and 6 deletions

View File

@ -1213,10 +1213,18 @@ public class Block {
// Apply fix for block 212937 but fix will be rolled back before we exit method
Block212937.processFix(this);
}
else if (this.blockData.getHeight() == 1333492) {
// Apply fix for block 1333492 but fix will be rolled back before we exit method
Block1333492.processFix(this);
}
else if (InvalidNameRegistrationBlocks.isAffectedBlock(this.blockData.getHeight())) {
// Apply fix for affected name registration blocks, but fix will be rolled back before we exit method
InvalidNameRegistrationBlocks.processFix(this);
}
else if (InvalidBalanceBlocks.isAffectedBlock(this.blockData.getHeight())) {
// Apply fix for affected balance blocks, but fix will be rolled back before we exit method
InvalidBalanceBlocks.processFix(this);
}
for (Transaction transaction : this.getTransactions()) {
TransactionData transactionData = transaction.getTransactionData();
@ -1464,12 +1472,21 @@ public class Block {
// Distribute block rewards, including transaction fees, before transactions processed
processBlockRewards();
if (this.blockData.getHeight() == 212937)
if (this.blockData.getHeight() == 212937) {
// Apply fix for block 212937
Block212937.processFix(this);
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
}
else if (this.blockData.getHeight() == 1333492) {
// Apply fix for block 1333492
Block1333492.processFix(this);
}
else if (InvalidBalanceBlocks.isAffectedBlock(this.blockData.getHeight())) {
// Apply fix for affected balance blocks
InvalidBalanceBlocks.processFix(this);
}
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height()) {
SelfSponsorshipAlgoV1Block.processAccountPenalties(this);
}
}
// We're about to (test-)process a batch of transactions,
@ -1726,12 +1743,21 @@ public class Block {
// Invalidate expandedAccounts as they may have changed due to orphaning TRANSFER_PRIVS transactions, etc.
this.cachedExpandedAccounts = null;
if (this.blockData.getHeight() == 212937)
if (this.blockData.getHeight() == 212937) {
// Revert fix for block 212937
Block212937.orphanFix(this);
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
}
else if (this.blockData.getHeight() == 1333492) {
// Revert fix for block 1333492
Block1333492.orphanFix(this);
}
else if (InvalidBalanceBlocks.isAffectedBlock(this.blockData.getHeight())) {
// Revert fix for affected balance blocks
InvalidBalanceBlocks.orphanFix(this);
}
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height()) {
SelfSponsorshipAlgoV1Block.orphanAccountPenalties(this);
}
// Block rewards, including transaction fees, removed after transactions undone
orphanBlockRewards();

View File

@ -0,0 +1,101 @@
package org.qortal.block;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.repository.DataException;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.UnmarshalException;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;
import java.io.InputStream;
import java.util.List;
import java.util.stream.Collectors;
/**
* Block 1333492
* <p>
* As described in InvalidBalanceBlocks.java, legacy bugs caused a small drift in account balances.
* This block adjusts any remaining differences between a clean reindex/resync and a recent bootstrap.
* <p>
* The block height 1333492 isn't significant - it's simply the height of a recent bootstrap at the
* time of development, so that the account balances could be accessed and compared against the same
* block in a reindexed db.
* <p>
* As with InvalidBalanceBlocks, the discrepancies are insignificant, except for a single
* account which has a 3.03 QORT discrepancy. This was due to the account being the first recipient
* of a name sale and encountering an early bug in this area.
* <p>
* The total offset for this block is 3.02816514 QORT.
*/
public final class Block1333492 {
private static final Logger LOGGER = LogManager.getLogger(Block1333492.class);
private static final String ACCOUNT_DELTAS_SOURCE = "block-1333492-deltas.json";
private static final List<AccountBalanceData> accountDeltas = readAccountDeltas();
private Block1333492() {
/* Do not instantiate */
}
@SuppressWarnings("unchecked")
private static List<AccountBalanceData> readAccountDeltas() {
Unmarshaller unmarshaller;
try {
// Create JAXB context aware of classes we need to unmarshal
JAXBContext jc = JAXBContextFactory.createContext(new Class[] {
AccountBalanceData.class
}, null);
// Create unmarshaller
unmarshaller = jc.createUnmarshaller();
// Set the unmarshaller media type to JSON
unmarshaller.setProperty(UnmarshallerProperties.MEDIA_TYPE, "application/json");
// Tell unmarshaller that there's no JSON root element in the JSON input
unmarshaller.setProperty(UnmarshallerProperties.JSON_INCLUDE_ROOT, false);
} catch (JAXBException e) {
String message = "Failed to setup unmarshaller to read block 1333492 deltas";
LOGGER.error(message, e);
throw new RuntimeException(message, e);
}
ClassLoader classLoader = BlockChain.class.getClassLoader();
InputStream in = classLoader.getResourceAsStream(ACCOUNT_DELTAS_SOURCE);
StreamSource jsonSource = new StreamSource(in);
try {
// Attempt to unmarshal JSON stream to BlockChain config
return (List<AccountBalanceData>) unmarshaller.unmarshal(jsonSource, AccountBalanceData.class).getValue();
} catch (UnmarshalException e) {
String message = "Failed to parse block 1333492 deltas";
LOGGER.error(message, e);
throw new RuntimeException(message, e);
} catch (JAXBException e) {
String message = "Unexpected JAXB issue while processing block 1333492 deltas";
LOGGER.error(message, e);
throw new RuntimeException(message, e);
}
}
public static void processFix(Block block) throws DataException {
block.repository.getAccountRepository().modifyAssetBalances(accountDeltas);
}
public static void orphanFix(Block block) throws DataException {
// Create inverse deltas
List<AccountBalanceData> inverseDeltas = accountDeltas.stream()
.map(delta -> new AccountBalanceData(delta.getAddress(), delta.getAssetId(), 0 - delta.getBalance()))
.collect(Collectors.toList());
block.repository.getAccountRepository().modifyAssetBalances(inverseDeltas);
}
}

View File

@ -0,0 +1,134 @@
package org.qortal.block;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.repository.DataException;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.UnmarshalException;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;
import java.io.InputStream;
import java.util.*;
import java.util.stream.Collectors;
/**
* Due to various bugs - which have been fixed - a small amount of balance drift occurred
* in the chainstate of running nodes and bootstraps, when compared with a clean sync from genesis.
* This resulted in a significant number of invalid transactions in the chain history due to
* subtle balance discrepancies. The sum of all discrepancies that resulted in an invalid
* transaction is 0.00198322 QORT, so despite the large quantity of transactions, they
* represent an insignificant amount when summed.
* <p>
* This class is responsible for retroactively fixing all the past transactions which
* are invalid due to the balance discrepancies.
*/
public final class InvalidBalanceBlocks {
private static final Logger LOGGER = LogManager.getLogger(InvalidBalanceBlocks.class);
private static final String ACCOUNT_DELTAS_SOURCE = "invalid-transaction-balance-deltas.json";
private static final List<AccountBalanceData> accountDeltas = readAccountDeltas();
private static final List<Integer> affectedHeights = getAffectedHeights();
private InvalidBalanceBlocks() {
/* Do not instantiate */
}
@SuppressWarnings("unchecked")
private static List<AccountBalanceData> readAccountDeltas() {
Unmarshaller unmarshaller;
try {
// Create JAXB context aware of classes we need to unmarshal
JAXBContext jc = JAXBContextFactory.createContext(new Class[] {
AccountBalanceData.class
}, null);
// Create unmarshaller
unmarshaller = jc.createUnmarshaller();
// Set the unmarshaller media type to JSON
unmarshaller.setProperty(UnmarshallerProperties.MEDIA_TYPE, "application/json");
// Tell unmarshaller that there's no JSON root element in the JSON input
unmarshaller.setProperty(UnmarshallerProperties.JSON_INCLUDE_ROOT, false);
} catch (JAXBException e) {
String message = "Failed to setup unmarshaller to read block 212937 deltas";
LOGGER.error(message, e);
throw new RuntimeException(message, e);
}
ClassLoader classLoader = BlockChain.class.getClassLoader();
InputStream in = classLoader.getResourceAsStream(ACCOUNT_DELTAS_SOURCE);
StreamSource jsonSource = new StreamSource(in);
try {
// Attempt to unmarshal JSON stream to BlockChain config
return (List<AccountBalanceData>) unmarshaller.unmarshal(jsonSource, AccountBalanceData.class).getValue();
} catch (UnmarshalException e) {
String message = "Failed to parse balance deltas";
LOGGER.error(message, e);
throw new RuntimeException(message, e);
} catch (JAXBException e) {
String message = "Unexpected JAXB issue while processing balance deltas";
LOGGER.error(message, e);
throw new RuntimeException(message, e);
}
}
private static List<Integer> getAffectedHeights() {
List<Integer> heights = new ArrayList<>();
for (AccountBalanceData accountBalanceData : accountDeltas) {
if (!heights.contains(accountBalanceData.getHeight())) {
heights.add(accountBalanceData.getHeight());
}
}
return heights;
}
private static List<AccountBalanceData> getAccountDeltasAtHeight(int height) {
return accountDeltas.stream().filter(a -> a.getHeight() == height).collect(Collectors.toList());
}
public static boolean isAffectedBlock(int height) {
return affectedHeights.contains(Integer.valueOf(height));
}
public static void processFix(Block block) throws DataException {
Integer blockHeight = block.getBlockData().getHeight();
List<AccountBalanceData> deltas = getAccountDeltasAtHeight(blockHeight);
if (deltas == null) {
throw new DataException(String.format("Unable to lookup invalid balance data for block height %d", blockHeight));
}
block.repository.getAccountRepository().modifyAssetBalances(deltas);
LOGGER.info("Applied balance patch for block {}", blockHeight);
}
public static void orphanFix(Block block) throws DataException {
Integer blockHeight = block.getBlockData().getHeight();
List<AccountBalanceData> deltas = getAccountDeltasAtHeight(blockHeight);
if (deltas == null) {
throw new DataException(String.format("Unable to lookup invalid balance data for block height %d", blockHeight));
}
// Create inverse delta(s)
for (AccountBalanceData accountBalanceData : deltas) {
AccountBalanceData inverseBalanceData = new AccountBalanceData(accountBalanceData.getAddress(), accountBalanceData.getAssetId(), -accountBalanceData.getBalance());
block.repository.getAccountRepository().modifyAssetBalances(List.of(inverseBalanceData));
}
LOGGER.info("Reverted balance patch for block {}", blockHeight);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff