diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 5f6e1641..798a4f91 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -1104,6 +1104,10 @@ public class Block { // Create repository savepoint here so we can rollback to it after testing transactions repository.setSavepoint(); + if (this.blockData.getHeight() == 212937) + // Apply fix for block 212937 but fix will be rolled back before we exit method + Block212937.processFix(this); + for (Transaction transaction : this.getTransactions()) { TransactionData transactionData = transaction.getTransactionData(); @@ -1308,6 +1312,9 @@ public class Block { // Distribute block rewards, including transaction fees, before transactions processed processBlockRewards(); + if (this.blockData.getHeight() == 212937) + // Apply fix for block 212937 + Block212937.processFix(this); } // We're about to (test-)process a batch of transactions, @@ -1542,6 +1549,10 @@ 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) + // Revert fix for block 212937 + Block212937.orphanFix(this); + // Block rewards, including transaction fees, removed after transactions undone orphanBlockRewards(); diff --git a/src/main/java/org/qortal/block/Block212937.java b/src/main/java/org/qortal/block/Block212937.java new file mode 100644 index 00000000..a53c9d31 --- /dev/null +++ b/src/main/java/org/qortal/block/Block212937.java @@ -0,0 +1,153 @@ +package org.qortal.block; + +import java.io.InputStream; +import java.util.List; +import java.util.stream.Collectors; + +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 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; + +/** + * Block 212937 + *
+ * Somehow a node minted a version of block 212937 that contained one transaction: + * a PAYMENT transaction that attempted to spend more QORT than that account had as QORT balance. + *
+ * This invalid transaction made block 212937 (rightly) invalid to several nodes, + * which refused to use that block. + * However, it seems there were no other nodes minting an alternative, valid block at that time + * and so the chain stalled for several nodes in the network. + *
+ * Additionally, the invalid block 212937 affected all new installations, regardless of whether + * they synchronized from scratch (block 1) or used an 'official release' bootstrap. + *
+ * After lengthy diagnosis, it was discovered that + * the invalid transaction seemed to rely on incorrect balances in a corrupted database. + * Copies of DB files containing the broken chain were also shared around, exacerbating the problem. + *
+ * There were three options: + *
+ * Option 1 was highly undesirable due to knock-on effects from wiping 700+ transactions, some of which + * might have affect cross-chain trades, although there were no cross-chain trade completed during + * the decision period. + *
+ * Option 3 was essentially a slightly better version of option 1 and rejected for similar reasons. + * Attempts at option 3 also rapidly hit cumulative problems with every replacement block due to + * differing block timestamps making some transactions, and then even some blocks themselves, invalid. + *
+ * This class is the implementation of option 2. + *
+ * The change in account balances are relatively small, see block-212937-deltas.json resource + * for actual values. These values were obtained by exporting the AccountBalances table from + * both versions of the database with chain at block 212936, and then comparing. The values were also + * tested by syncing both databases up to block 225500, re-exporting and re-comparing. + *
+ * The invalid block 212937 signature is: 2J3GVJjv...qavh6KkQ. + *
+ * The invalid transaction in block 212937 is: + *
+ *
+ *
+ {
+ "amount" : "0.10788294",
+ "approvalStatus" : "NOT_REQUIRED",
+ "blockHeight" : 212937,
+ "creatorAddress" : "QLdw5uabviLJgRGkRiydAFmAtZzxHfNXSs",
+ "fee" : "0.00100000",
+ "recipient" : "QZi1mNHDbiLvsytxTgxDr9nhJe4pNZaWpw",
+ "reference" : "J6JukdTVuXZ3JYbHatfZzwxG2vSiZwVCPDzW5K7PsVQKRj8XZeDtqnkGCGGjaSQZ9bQMtV44ky88NnGM4YBQKU6",
+ "senderPublicKey" : "DBFfbD2M3uh4jPE5PaUcZVvNPfrrJzVB7seeEtBn5SPs",
+ "signature" : "qkitxdCEEnKt8w6wRfFixtErbXsxWE6zG2ESNhpqBdScikV1WxeA6WZTTMJVV4tCeZdBFXw3V1X5NVztv6LirWK",
+ "timestamp" : 1607863074904,
+ "txGroupId" : 0,
+ "type" : "PAYMENT"
+ }
+
+ * Account QLdw5uabviLJgRGkRiydAFmAtZzxHfNXSs attempted to spend 0.10888294 (including fees) + * when their QORT balance was really only 0.10886665. + *
+ * However, on the broken DB nodes, their balance
+ * seemed to be 0.10890293 which was sufficient to make the transaction valid.
+ */
+public final class Block212937 {
+
+ private static final Logger LOGGER = LogManager.getLogger(Block212937.class);
+ private static final String ACCOUNT_DELTAS_SOURCE = "block-212937-deltas.json";
+
+ private static final List