Compare commits

...

4 Commits

Author SHA1 Message Date
CalDescent
c735086db8 Finalized the reindex feature. 2023-08-06 14:45:37 +01:00
CalDescent
cd792fff55 Chain history cleanups to correct legacy balance bugs. 2023-08-06 14:45:37 +01:00
CalDescent
f808e80045 Added cancelSellNameValidationTimestamp, to retroactively fix validation of some legacy duplicate cancel sell name transactions.
These were caused by a bug in the name rebuilding code, which has since been fixed.

Affected transactions:

CancelSellNameTransaction:74 - Error during transaction validation, tx 3FLFa9LuYS3tJ3bHB6mTDnLcAHBbcNHhQhvPT8wvcW14w59TGiJ9NabGe7HzG7XVZRpuhRQoaFDfDfJPcdrU44ry: NAME_NOT_FOR_SALE - 1177021 - 1676828446023
CancelSellNameTransaction:74 - Error during transaction validation, tx 4ZVREx4ZnBn5nfFCXvpjCXLFDV9aSqYcCqhCuYJQ2bf4h4mH6wkuAKGGgF9d2xWZWYY5ujFR2E2PBkg2zTzRhf6m: NAME_NOT_FOR_SALE - 1178222 - 1676841112553
CancelSellNameTransaction:74 - Error during transaction validation, tx 3caxAKM291kUVLmsAfpbsnrgwk9VZdTRLyt86iVjsFzhJs22gGdKf26fJqpzBt6czqhoTosPH9z4o14nQ56cZpjM: NAME_NOT_FOR_SALE - 1179201 - 1676986362069
CancelSellNameTransaction:74 - Error during transaction validation, tx 3FLFa9LuYS3tJ3bHB6mTDnLcAHBbcNHhQhvPT8wvcW14w59TGiJ9NabGe7HzG7XVZRpuhRQoaFDfDfJPcdrU44ry: NAME_NOT_FOR_SALE - 1177021 - 1676828446023
CancelSellNameTransaction:74 - Error during transaction validation, tx 4ZVREx4ZnBn5nfFCXvpjCXLFDV9aSqYcCqhCuYJQ2bf4h4mH6wkuAKGGgF9d2xWZWYY5ujFR2E2PBkg2zTzRhf6m: NAME_NOT_FOR_SALE - 1178222 - 1676841112553
CancelSellNameTransaction:74 - Error during transaction validation, tx 3caxAKM291kUVLmsAfpbsnrgwk9VZdTRLyt86iVjsFzhJs22gGdKf26fJqpzBt6czqhoTosPH9z4o14nQ56cZpjM: NAME_NOT_FOR_SALE - 1179201 - 1676986362069
2023-08-06 14:45:37 +01:00
CalDescent
5b1f05d1d9 Added initial version of ReindexManager 2023-08-06 14:45:37 +01:00
10 changed files with 8437 additions and 11 deletions

View File

@ -20,7 +20,6 @@ import java.time.LocalDate;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@ -38,7 +37,6 @@ import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.RollingFileAppender;
import org.json.JSONArray;
import org.json.JSONObject;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.api.*;
@ -57,6 +55,7 @@ import org.qortal.network.Network;
import org.qortal.network.Peer;
import org.qortal.network.PeerAddress;
import org.qortal.repository.DataException;
import org.qortal.repository.ReindexManager;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
@ -876,6 +875,48 @@ public class AdminResource {
}
}
@POST
@Path("/repository/reindex")
@Operation(
summary = "Reindex repository",
description = "Rebuilds all transactions and balances from archived blocks. Warning: takes around 1 week, and the core will not function normally during this time. If 'false' is returned, the database may be left in an inconsistent state, requiring another reindex or a bootstrap to correct it.",
responses = {
@ApiResponse(
description = "\"true\"",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String reindex(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
Security.checkApiCallAllowed(request);
try {
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
blockchainLock.lockInterruptibly();
try {
ReindexManager reindexManager = new ReindexManager();
reindexManager.reindex();
return "true";
} catch (DataException e) {
LOGGER.info("DataException when reindexing: {}", e.getMessage());
} finally {
blockchainLock.unlock();
}
} catch (InterruptedException e) {
// We couldn't lock blockchain to perform reindex
return "false";
}
return "false";
}
@DELETE
@Path("/repository")
@Operation(

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

@ -79,7 +79,8 @@ public class BlockChain {
selfSponsorshipAlgoV1Height,
feeValidationFixTimestamp,
chatReferenceTimestamp,
arbitraryOptionalFeeTimestamp;
arbitraryOptionalFeeTimestamp,
cancelSellNameValidationTimestamp;
}
// Custom transaction fees
@ -527,6 +528,10 @@ public class BlockChain {
return this.featureTriggers.get(FeatureTrigger.arbitraryOptionalFeeTimestamp.name()).longValue();
}
public long getCancelSellNameValidationTimestamp() {
return this.featureTriggers.get(FeatureTrigger.cancelSellNameValidationTimestamp.name()).longValue();
}
// More complex getters for aspects that change by height or timestamp

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);
}
}

View File

@ -0,0 +1,213 @@
package org.qortal.repository;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.block.Block;
import org.qortal.block.GenesisBlock;
import org.qortal.controller.Controller;
import org.qortal.data.block.BlockArchiveData;
import org.qortal.data.block.BlockData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.settings.Settings;
import org.qortal.transaction.Transaction;
import org.qortal.transform.block.BlockTransformation;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import java.util.concurrent.TimeoutException;
public class ReindexManager {
private static final Logger LOGGER = LogManager.getLogger(ReindexManager.class);
private Repository repository;
private final int pruneAndTrimBlockInterval = 2000;
private final int maintenanceBlockInterval = 50000;
private boolean resume = false;
public ReindexManager() {
}
public void reindex() throws DataException {
try {
this.runPreChecks();
this.rebuildRepository();
try (final Repository repository = RepositoryManager.getRepository()) {
this.repository = repository;
this.requestCheckpoint();
this.processGenesisBlock();
this.processBlocks();
}
} catch (InterruptedException e) {
throw new DataException("Interrupted before complete");
}
}
private void runPreChecks() throws DataException, InterruptedException {
LOGGER.info("Running pre-checks...");
if (Settings.getInstance().isTopOnly()) {
throw new DataException("Reindexing not supported in top-only mode. Please bootstrap or resync from genesis.");
}
if (Settings.getInstance().isLite()) {
throw new DataException("Reindexing not supported in lite mode.");
}
while (NTP.getTime() == null) {
LOGGER.info("Waiting for NTP...");
Thread.sleep(5000L);
}
}
private void rebuildRepository() throws DataException {
if (resume) {
return;
}
LOGGER.info("Rebuilding repository...");
RepositoryManager.rebuild();
}
private void requestCheckpoint() {
RepositoryManager.setRequestedCheckpoint(Boolean.TRUE);
}
private void processGenesisBlock() throws DataException, InterruptedException {
if (resume) {
return;
}
LOGGER.info("Processing genesis block...");
GenesisBlock genesisBlock = GenesisBlock.getInstance(repository);
// Add Genesis Block to blockchain
genesisBlock.process();
this.repository.saveChanges();
}
private void processBlocks() throws DataException {
LOGGER.info("Processing blocks...");
int height = this.repository.getBlockRepository().getBlockchainHeight();
while (true) {
height++;
boolean processed = this.processBlock(height);
if (!processed) {
LOGGER.info("Block {} couldn't be processed. If this is the last archived block, then the process is complete.", height);
break; // TODO: check if complete
}
// Prune and trim regularly, leaving a buffer
if (height >= pruneAndTrimBlockInterval*2 && height % pruneAndTrimBlockInterval == 0) {
int startHeight = Math.max(height - pruneAndTrimBlockInterval*2, 2);
int endHeight = height - pruneAndTrimBlockInterval;
LOGGER.info("Pruning and trimming blocks {} to {}...", startHeight, endHeight);
this.repository.getATRepository().rebuildLatestAtStates(height - 250);
this.repository.saveChanges();
this.prune(startHeight, endHeight);
this.trim(startHeight, endHeight);
}
// Run repository maintenance regularly, to keep blockchain.data size down
if (height % maintenanceBlockInterval == 0) {
this.runRepositoryMaintenance();
}
}
}
private boolean processBlock(int height) throws DataException {
Block block = this.fetchBlock(height);
if (block == null) {
return false;
}
// Transactions are stored without approval status so determine that now
for (Transaction transaction : block.getTransactions())
transaction.setInitialApprovalStatus();
// It's best not to run preProcess() until there is a reason to
// block.preProcess();
Block.ValidationResult validationResult = block.isValid();
if (validationResult != Block.ValidationResult.OK) {
throw new DataException(String.format("Invalid block at height %d: %s", height, validationResult));
}
// Save transactions attached to this block
for (Transaction transaction : block.getTransactions()) {
TransactionData transactionData = transaction.getTransactionData();
this.repository.getTransactionRepository().save(transactionData);
}
block.process();
LOGGER.info(String.format("Reindexed block height %d, sig %.8s", block.getBlockData().getHeight(), Base58.encode(block.getBlockData().getSignature())));
// Add to block archive table, since this originated from the archive but the chainstate has to be rebuilt
this.addToBlockArchive(block.getBlockData());
this.repository.saveChanges();
Controller.getInstance().onNewBlock(block.getBlockData());
return true;
}
private Block fetchBlock(int height) {
BlockTransformation b = BlockArchiveReader.getInstance().fetchBlockAtHeight(height);
if (b != null) {
if (b.getAtStatesHash() != null) {
return new Block(this.repository, b.getBlockData(), b.getTransactions(), b.getAtStatesHash());
}
else {
return new Block(this.repository, b.getBlockData(), b.getTransactions(), b.getAtStates());
}
}
return null;
}
private void addToBlockArchive(BlockData blockData) throws DataException {
// Write the signature and height into the BlockArchive table
BlockArchiveData blockArchiveData = new BlockArchiveData(blockData);
this.repository.getBlockArchiveRepository().save(blockArchiveData);
this.repository.getBlockArchiveRepository().setBlockArchiveHeight(blockData.getHeight()+1);
this.repository.saveChanges();
}
private void prune(int startHeight, int endHeight) throws DataException {
this.repository.getBlockRepository().pruneBlocks(startHeight, endHeight);
this.repository.getATRepository().pruneAtStates(startHeight, endHeight);
this.repository.getATRepository().setAtPruneHeight(endHeight+1);
this.repository.saveChanges();
}
private void trim(int startHeight, int endHeight) throws DataException {
this.repository.getBlockRepository().trimOldOnlineAccountsSignatures(startHeight, endHeight);
int count = 1; // Any number greater than 0
while (count > 0) {
count = this.repository.getATRepository().trimAtStates(startHeight, endHeight, Settings.getInstance().getAtStatesTrimLimit());
}
this.repository.getBlockRepository().setBlockPruneHeight(endHeight+1);
this.repository.getATRepository().setAtTrimHeight(endHeight+1);
this.repository.saveChanges();
}
private void runRepositoryMaintenance() throws DataException {
try {
this.repository.performPeriodicMaintenance(1000L);
} catch (TimeoutException e) {
LOGGER.info("Timed out waiting for repository before running maintenance");
}
}
}

View File

@ -5,6 +5,7 @@ import java.util.List;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.CancelSellNameTransactionData;
@ -65,7 +66,9 @@ public class CancelSellNameTransaction extends Transaction {
// Check name is currently for sale
if (!nameData.isForSale())
return ValidationResult.NAME_NOT_FOR_SALE;
// Only validate after feature-trigger timestamp, due to a small number of double cancelations in the chain history
if (this.cancelSellNameTransactionData.getTimestamp() > BlockChain.getInstance().getCancelSellNameValidationTimestamp())
return ValidationResult.NAME_NOT_FOR_SALE;
// Check transaction creator matches name's current owner
Account owner = getOwner();

File diff suppressed because it is too large Load Diff

View File

@ -86,7 +86,8 @@
"selfSponsorshipAlgoV1Height": 1092400,
"feeValidationFixTimestamp": 1671918000000,
"chatReferenceTimestamp": 1674316800000,
"arbitraryOptionalFeeTimestamp": 1680278400000
"arbitraryOptionalFeeTimestamp": 1680278400000,
"cancelSellNameValidationTimestamp": 1676986362069
},
"checkpoints": [
{ "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" }

File diff suppressed because it is too large Load Diff