diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index bacd7825..437a48ab 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -100,6 +100,13 @@ public class BlockChain { /** Whether only one registered name is allowed per account. */ private boolean oneNamePerAccount = false; + /** Checkpoints */ + public static class Checkpoint { + public int height; + public String signature; + } + private List checkpoints; + /** Block rewards by block height */ public static class RewardByHeight { public int height; @@ -381,6 +388,10 @@ public class BlockChain { return this.oneNamePerAccount; } + public List getCheckpoints() { + return this.checkpoints; + } + public List getBlockRewardsByHeight() { return this.rewardsByHeight; } @@ -679,6 +690,7 @@ public class BlockChain { boolean isTopOnly = Settings.getInstance().isTopOnly(); boolean archiveEnabled = Settings.getInstance().isArchiveEnabled(); + boolean isLite = Settings.getInstance().isLite(); boolean canBootstrap = Settings.getInstance().getBootstrap(); boolean needsArchiveRebuild = false; BlockData chainTip; @@ -699,23 +711,45 @@ public class BlockChain { } } } - } - boolean hasBlocks = (chainTip != null && chainTip.getHeight() > 1); - - if (isTopOnly && hasBlocks) { - // Top-only mode is enabled and we have blocks, so it's possible that the genesis block has been pruned - // It's best not to validate it, and there's no real need to - } else { - // Check first block is Genesis Block - if (!isGenesisBlockValid() || needsArchiveRebuild) { - try { - rebuildBlockchain(); + // Validate checkpoints + // Limited to topOnly nodes for now, in order to reduce risk, and to solve a real-world problem with divergent topOnly nodes + // TODO: remove the isTopOnly conditional below once this feature has had more testing time + if (isTopOnly && !isLite) { + List checkpoints = BlockChain.getInstance().getCheckpoints(); + for (Checkpoint checkpoint : checkpoints) { + BlockData blockData = repository.getBlockRepository().fromHeight(checkpoint.height); + if (blockData == null) { + // Try the archive + blockData = repository.getBlockArchiveRepository().fromHeight(checkpoint.height); + } + if (blockData == null) { + LOGGER.trace("Couldn't find block for height {}", checkpoint.height); + // This is likely due to the block being pruned, so is safe to ignore. + // Continue, as there might be other blocks we can check more definitively. + continue; + } - } catch (InterruptedException e) { - throw new DataException(String.format("Interrupted when trying to rebuild blockchain: %s", e.getMessage())); + byte[] signature = Base58.decode(checkpoint.signature); + if (!Arrays.equals(signature, blockData.getSignature())) { + LOGGER.info("Error: block at height {} with signature {} doesn't match checkpoint sig: {}. Bootstrapping...", checkpoint.height, Base58.encode(blockData.getSignature()), checkpoint.signature); + needsArchiveRebuild = true; + break; + } + LOGGER.info("Block at height {} matches checkpoint signature", blockData.getHeight()); } } + + } + + // Check first block is Genesis Block + if (!isGenesisBlockValid() || needsArchiveRebuild) { + try { + rebuildBlockchain(); + + } catch (InterruptedException e) { + throw new DataException(String.format("Interrupted when trying to rebuild blockchain: %s", e.getMessage())); + } } // We need to create a new connection, as the previous repository and its connections may be been diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index aa6cd73b..f48958eb 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -87,6 +87,9 @@ "feeValidationFixTimestamp": 1671918000000, "chatReferenceTimestamp": 1674316800000 }, + "checkpoints": [ + { "height": 1131800, "signature": "EpRam4PLdKzULMp7xNU7XG964AKfioG3g1k7cxwxWXnXspPwnjfF6UncEz4feuSA9mr1vW5d3YQPGruXYjj4vciSh4SPj5iWRxkHRWFeRpQnmVUyaVumuBTwM8nnLKJTdtkZnd6d8Mc5mVFdHs6EwLBTY4HECoRcbo4e4FwkfqVon4M" } + ], "genesisInfo": { "version": 4, "timestamp": "1593450000000",