From 1676098abeda7b0d3c0726c8d7ef93199c01fbd7 Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Wed, 13 Nov 2024 03:06:58 -0500 Subject: [PATCH 01/16] Add missing feature triggers to unit tests --- src/test/resources/test-chain-v2-block-timestamps.json | 7 ++++++- .../resources/test-chain-v2-disable-reference.json | 7 ++++++- src/test/resources/test-chain-v2-founder-rewards.json | 7 ++++++- src/test/resources/test-chain-v2-leftover-reward.json | 7 ++++++- src/test/resources/test-chain-v2-minting.json | 7 ++++++- src/test/resources/test-chain-v2-penalty-fix.json | 10 ++++++++-- .../resources/test-chain-v2-qora-holder-extremes.json | 7 ++++++- .../resources/test-chain-v2-qora-holder-reduction.json | 7 ++++++- src/test/resources/test-chain-v2-qora-holder.json | 7 ++++++- src/test/resources/test-chain-v2-reward-levels.json | 7 ++++++- src/test/resources/test-chain-v2-reward-scaling.json | 7 ++++++- src/test/resources/test-chain-v2-reward-shares.json | 7 ++++++- .../test-chain-v2-self-sponsorship-algo-v1.json | 7 ++++++- .../test-chain-v2-self-sponsorship-algo-v2.json | 7 ++++++- .../test-chain-v2-self-sponsorship-algo-v3.json | 7 ++++++- 15 files changed, 92 insertions(+), 16 deletions(-) diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json index 17fc80c4..b2f0119d 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -91,7 +91,12 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999999, + "groupMemberCheckHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json index 33054732..86ed264f 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -94,7 +94,12 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999999, + "groupMemberCheckHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index 577a07f1..d1b9c3c4 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -95,7 +95,12 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999999, + "groupMemberCheckHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index 82e4ace7..106ac7dd 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -95,7 +95,12 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999999, + "groupMemberCheckHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index 16032a9c..159b2dd7 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -95,7 +95,12 @@ "arbitraryOptionalFeeTimestamp": 9999999999999, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999999, + "groupMemberCheckHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-penalty-fix.json b/src/test/resources/test-chain-v2-penalty-fix.json index e62fc9f2..2266b032 100644 --- a/src/test/resources/test-chain-v2-penalty-fix.json +++ b/src/test/resources/test-chain-v2-penalty-fix.json @@ -85,14 +85,20 @@ "disableReferenceTimestamp": 9999999999999, "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 0, - "selfSponsorshipAlgoV1Height": 99999999, + "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, - "selfSponsorshipAlgoV2Height": 9999999, "disableTransferPrivsTimestamp": 9999999999500, "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999999, + "groupMemberCheckHeight": 9999999999999, "penaltyFixHeight": 5 }, "genesisInfo": { diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json index 3ec11942..6043f15c 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -95,7 +95,12 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999999, + "groupMemberCheckHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder-reduction.json b/src/test/resources/test-chain-v2-qora-holder-reduction.json index 2b8834ce..7727a283 100644 --- a/src/test/resources/test-chain-v2-qora-holder-reduction.json +++ b/src/test/resources/test-chain-v2-qora-holder-reduction.json @@ -96,7 +96,12 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999999, + "groupMemberCheckHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index ab96a243..6b9f9d54 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -95,7 +95,12 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999999, + "groupMemberCheckHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index 35535c75..6f0993d0 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -95,7 +95,12 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999999, + "groupMemberCheckHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index 616d0925..d1d4519a 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -95,7 +95,12 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 500, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999999, + "groupMemberCheckHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json index ec6ffd2e..69edc540 100644 --- a/src/test/resources/test-chain-v2-reward-shares.json +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -95,7 +95,12 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999999, + "groupMemberCheckHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json b/src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json index d0d989cf..ad2ad4b1 100644 --- a/src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json @@ -95,7 +95,12 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999999, + "groupMemberCheckHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json b/src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json index 5f09cb47..b2812c05 100644 --- a/src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json @@ -95,7 +95,12 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999999, + "groupMemberCheckHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json b/src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json index f7d1faa2..d65aa48e 100644 --- a/src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json @@ -95,7 +95,12 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999999, + "groupMemberCheckHeight": 9999999999999 }, "genesisInfo": { "version": 4, From 82d5d25c597d6d3f9f9e8c9a9c176c35df8c759b Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Wed, 13 Nov 2024 06:16:39 -0500 Subject: [PATCH 02/16] Add logging to block archive unit tests --- .../org/qortal/test/BlockArchiveV1Tests.java | 1049 ++++++++++------- .../org/qortal/test/BlockArchiveV2Tests.java | 876 ++++++++------ 2 files changed, 1112 insertions(+), 813 deletions(-) diff --git a/src/test/java/org/qortal/test/BlockArchiveV1Tests.java b/src/test/java/org/qortal/test/BlockArchiveV1Tests.java index a28bd28d..60eefa8e 100644 --- a/src/test/java/org/qortal/test/BlockArchiveV1Tests.java +++ b/src/test/java/org/qortal/test/BlockArchiveV1Tests.java @@ -34,441 +34,574 @@ import static org.junit.Assert.*; public class BlockArchiveV1Tests extends Common { - @Before - public void beforeTest() throws DataException, IllegalAccessException { - Common.useSettings("test-settings-v2-block-archive.json"); - NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset()); - this.deleteArchiveDirectory(); + @Before + public void beforeTest() throws DataException, IllegalAccessException { + Common.useSettings("test-settings-v2-block-archive.json"); + NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset()); + this.deleteArchiveDirectory(); - // Set default archive version to 1, so that archive builds in these tests use V2 - FieldUtils.writeField(Settings.getInstance(), "defaultArchiveVersion", 1, true); - } + // Set default archive version to 1, so that archive builds in these tests use V2 + FieldUtils.writeField(Settings.getInstance(), "defaultArchiveVersion", 1, true); + } - @After - public void afterTest() throws DataException { - this.deleteArchiveDirectory(); - } + @After + public void afterTest() throws DataException { + this.deleteArchiveDirectory(); + } + @Test + public void testWriter() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { + + System.out.println("Starting testWriter"); + + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); + + // 900 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); + repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); + + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); + assertEquals(900, maximumArchiveHeight); + + // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); + assertEquals(900 - 1, writer.getWrittenCount()); + + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + + System.out.println("testWriter completed successfully."); + } + } + + @Test + public void testWriterAndReader() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { + + System.out.println("Starting testWriterAndReader"); + + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); + + // 900 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); + repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); + + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); + assertEquals(900, maximumArchiveHeight); + + // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); + assertEquals(900 - 1, writer.getWrittenCount()); + + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + + // Read block 2 from the archive + System.out.println("Reading block 2 from the archive..."); + BlockArchiveReader reader = BlockArchiveReader.getInstance(); + BlockTransformation block2Info = reader.fetchBlockAtHeight(2); + BlockData block2ArchiveData = block2Info.getBlockData(); + + // Read block 2 from the repository + BlockData block2RepositoryData = repository.getBlockRepository().fromHeight(2); + + // Ensure the values match + System.out.println("Comparing block 2 data..."); + assertEquals(block2ArchiveData.getHeight(), block2RepositoryData.getHeight()); + assertArrayEquals(block2ArchiveData.getSignature(), block2RepositoryData.getSignature()); + + // Test some values in the archive + assertEquals(1, block2ArchiveData.getOnlineAccountsCount()); + + // Read block 900 from the archive + System.out.println("Reading block 900 from the archive..."); + BlockTransformation block900Info = reader.fetchBlockAtHeight(900); + BlockData block900ArchiveData = block900Info.getBlockData(); + + // Read block 900 from the repository + BlockData block900RepositoryData = repository.getBlockRepository().fromHeight(900); + + // Ensure the values match + System.out.println("Comparing block 900 data..."); + assertEquals(block900ArchiveData.getHeight(), block900RepositoryData.getHeight()); + assertArrayEquals(block900ArchiveData.getSignature(), block900RepositoryData.getSignature()); + + // Test some values in the archive + assertEquals(1, block900ArchiveData.getOnlineAccountsCount()); + + System.out.println("testWriterAndReader completed successfully."); + } + } + + @Test + public void testArchivedAtStates() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { + + System.out.println("Starting testArchivedAtStates"); + + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + byte[] creationBytes = AtUtils.buildSimpleAT(); + long fundingAmount = 1_00000000L; + DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + String atAddress = deployAtTransaction.getATAccount().getAddress(); + System.out.println("AT deployed at address: " + atAddress); + + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); + + // 9 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(10); + repository.getATRepository().setAtTrimHeight(10); + System.out.println("Set trim heights to 10."); + + // Check the max archive height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 9): " + maximumArchiveHeight); + assertEquals(9, maximumArchiveHeight); + + // Write blocks 2-9 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 8)"); + assertEquals(9 - 1, writer.getWrittenCount()); + + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(9 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + + // Check blocks 3-9 + System.out.println("Checking blocks 3 to 9..."); + for (Integer testHeight = 2; testHeight <= 9; testHeight++) { + + System.out.println("Reading block " + testHeight + " from the archive..."); + // Read a block from the archive + BlockArchiveReader reader = BlockArchiveReader.getInstance(); + BlockTransformation blockInfo = reader.fetchBlockAtHeight(testHeight); + BlockData archivedBlockData = blockInfo.getBlockData(); + ATStateData archivedAtStateData = blockInfo.getAtStates().isEmpty() ? null : blockInfo.getAtStates().get(0); + List archivedTransactions = blockInfo.getTransactions(); + + // Read the same block from the repository + BlockData repositoryBlockData = repository.getBlockRepository().fromHeight(testHeight); + ATStateData repositoryAtStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight); + + // Ensure the repository has full AT state data + assertNotNull(repositoryAtStateData.getStateHash()); + assertNotNull(repositoryAtStateData.getStateData()); + + // Check the archived AT state + if (testHeight == 2) { + System.out.println("Checking block " + testHeight + " AT state data (expected null)..."); + // Block 2 won't have an AT state hash because it's initial (and has the DEPLOY_AT in the same block) + assertNull(archivedAtStateData); + + assertEquals(1, archivedTransactions.size()); + assertEquals(Transaction.TransactionType.DEPLOY_AT, archivedTransactions.get(0).getType()); + } + else { + System.out.println("Checking block " + testHeight + " AT state data..."); + // For blocks 3+, ensure the archive has the AT state data, but not the hashes + assertNotNull(archivedAtStateData.getStateHash()); + assertNull(archivedAtStateData.getStateData()); + + // They also shouldn't have any transactions + assertTrue(archivedTransactions.isEmpty()); + } + + // Also check the online accounts count and height + assertEquals(1, archivedBlockData.getOnlineAccountsCount()); + assertEquals(testHeight, archivedBlockData.getHeight()); + + // Ensure the values match + assertEquals(archivedBlockData.getHeight(), repositoryBlockData.getHeight()); + assertArrayEquals(archivedBlockData.getSignature(), repositoryBlockData.getSignature()); + assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); + assertArrayEquals(archivedBlockData.getMinterSignature(), repositoryBlockData.getMinterSignature()); + assertEquals(archivedBlockData.getATCount(), repositoryBlockData.getATCount()); + assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); + assertArrayEquals(archivedBlockData.getReference(), repositoryBlockData.getReference()); + assertEquals(archivedBlockData.getTimestamp(), repositoryBlockData.getTimestamp()); + assertEquals(archivedBlockData.getATFees(), repositoryBlockData.getATFees()); + assertEquals(archivedBlockData.getTotalFees(), repositoryBlockData.getTotalFees()); + assertEquals(archivedBlockData.getTransactionCount(), repositoryBlockData.getTransactionCount()); + assertArrayEquals(archivedBlockData.getTransactionsSignature(), repositoryBlockData.getTransactionsSignature()); + + if (testHeight != 2) { + assertArrayEquals(archivedAtStateData.getStateHash(), repositoryAtStateData.getStateHash()); + } + } + + // Check block 10 (unarchived) + System.out.println("Checking block 10 (should not be in archive)..."); + BlockArchiveReader reader = BlockArchiveReader.getInstance(); + BlockTransformation blockInfo = reader.fetchBlockAtHeight(10); + assertNull(blockInfo); + + System.out.println("testArchivedAtStates completed successfully."); + } + + } + + @Test + public void testArchiveAndPrune() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { + + System.out.println("Starting testArchiveAndPrune"); + + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + byte[] creationBytes = AtUtils.buildSimpleAT(); + long fundingAmount = 1_00000000L; + AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); + + // Assume 900 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); + repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); + + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); + assertEquals(900, maximumArchiveHeight); + + // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); + assertEquals(900 - 1, writer.getWrittenCount()); + + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(901); + repository.saveChanges(); + assertEquals(901, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + + // Ensure the SQL repository contains blocks 2 and 900... + assertNotNull(repository.getBlockRepository().fromHeight(2)); + assertNotNull(repository.getBlockRepository().fromHeight(900)); + System.out.println("Blocks 2 and 900 exist in the repository."); + + // Prune all the archived blocks + System.out.println("Pruning blocks 2 to 900..."); + int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 900); + assertEquals(900-1, numBlocksPruned); + repository.getBlockRepository().setBlockPruneHeight(901); + + // Prune the AT states for the archived blocks + System.out.println("Pruning AT states up to height 900..."); + repository.getATRepository().rebuildLatestAtStates(900); + repository.saveChanges(); + int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 900); + assertEquals(900-2, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state + repository.getATRepository().setAtPruneHeight(901); + + // Now ensure the SQL repository is missing blocks 2 and 900... + assertNull(repository.getBlockRepository().fromHeight(2)); + assertNull(repository.getBlockRepository().fromHeight(900)); + System.out.println("Blocks 2 and 900 have been pruned from the repository."); + + // ... but it's not missing blocks 1 and 901 (we don't prune the genesis block) + assertNotNull(repository.getBlockRepository().fromHeight(1)); + assertNotNull(repository.getBlockRepository().fromHeight(901)); + System.out.println("Blocks 1 and 901 still exist in the repository."); + + // Validate the latest block height in the repository + int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); + assertEquals(1002, lastBlockHeight); + + System.out.println("testArchiveAndPrune completed successfully."); + } + } + @Test - public void testWriter() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { - - // Mint some blocks so that we are able to archive them later - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - } - - // 900 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); - repository.getATRepository().setAtTrimHeight(901); - - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - assertEquals(900, maximumArchiveHeight); - - // Write blocks 2-900 to the archive - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - - // Make sure that the archive contains the correct number of blocks - assertEquals(900 - 1, writer.getWrittenCount()); - - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - } - } - - @Test - public void testWriterAndReader() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { - - // Mint some blocks so that we are able to archive them later - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - } - - // 900 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); - repository.getATRepository().setAtTrimHeight(901); - - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - assertEquals(900, maximumArchiveHeight); - - // Write blocks 2-900 to the archive - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - - // Make sure that the archive contains the correct number of blocks - assertEquals(900 - 1, writer.getWrittenCount()); - - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - - // Read block 2 from the archive - BlockArchiveReader reader = BlockArchiveReader.getInstance(); - BlockTransformation block2Info = reader.fetchBlockAtHeight(2); - BlockData block2ArchiveData = block2Info.getBlockData(); - - // Read block 2 from the repository - BlockData block2RepositoryData = repository.getBlockRepository().fromHeight(2); - - // Ensure the values match - assertEquals(block2ArchiveData.getHeight(), block2RepositoryData.getHeight()); - assertArrayEquals(block2ArchiveData.getSignature(), block2RepositoryData.getSignature()); - - // Test some values in the archive - assertEquals(1, block2ArchiveData.getOnlineAccountsCount()); - - // Read block 900 from the archive - BlockTransformation block900Info = reader.fetchBlockAtHeight(900); - BlockData block900ArchiveData = block900Info.getBlockData(); - - // Read block 900 from the repository - BlockData block900RepositoryData = repository.getBlockRepository().fromHeight(900); - - // Ensure the values match - assertEquals(block900ArchiveData.getHeight(), block900RepositoryData.getHeight()); - assertArrayEquals(block900ArchiveData.getSignature(), block900RepositoryData.getSignature()); - - // Test some values in the archive - assertEquals(1, block900ArchiveData.getOnlineAccountsCount()); - - } - } - - @Test - public void testArchivedAtStates() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { - - // Deploy an AT so that we have AT state data - PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); - byte[] creationBytes = AtUtils.buildSimpleAT(); - long fundingAmount = 1_00000000L; - DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); - String atAddress = deployAtTransaction.getATAccount().getAddress(); - - // Mint some blocks so that we are able to archive them later - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - } - - // 9 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(10); - repository.getATRepository().setAtTrimHeight(10); - - // Check the max archive height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - assertEquals(9, maximumArchiveHeight); - - // Write blocks 2-9 to the archive - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - - // Make sure that the archive contains the correct number of blocks - assertEquals(9 - 1, writer.getWrittenCount()); - - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(9 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - - // Check blocks 3-9 - for (Integer testHeight = 2; testHeight <= 9; testHeight++) { - - // Read a block from the archive - BlockArchiveReader reader = BlockArchiveReader.getInstance(); - BlockTransformation blockInfo = reader.fetchBlockAtHeight(testHeight); - BlockData archivedBlockData = blockInfo.getBlockData(); - ATStateData archivedAtStateData = blockInfo.getAtStates().isEmpty() ? null : blockInfo.getAtStates().get(0); - List archivedTransactions = blockInfo.getTransactions(); - - // Read the same block from the repository - BlockData repositoryBlockData = repository.getBlockRepository().fromHeight(testHeight); - ATStateData repositoryAtStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight); - - // Ensure the repository has full AT state data - assertNotNull(repositoryAtStateData.getStateHash()); - assertNotNull(repositoryAtStateData.getStateData()); - - // Check the archived AT state - if (testHeight == 2) { - // Block 2 won't have an AT state hash because it's initial (and has the DEPLOY_AT in the same block) - assertNull(archivedAtStateData); - - assertEquals(1, archivedTransactions.size()); - assertEquals(Transaction.TransactionType.DEPLOY_AT, archivedTransactions.get(0).getType()); - } - else { - // For blocks 3+, ensure the archive has the AT state data, but not the hashes - assertNotNull(archivedAtStateData.getStateHash()); - assertNull(archivedAtStateData.getStateData()); - - // They also shouldn't have any transactions - assertTrue(archivedTransactions.isEmpty()); - } - - // Also check the online accounts count and height - assertEquals(1, archivedBlockData.getOnlineAccountsCount()); - assertEquals(testHeight, archivedBlockData.getHeight()); - - // Ensure the values match - assertEquals(archivedBlockData.getHeight(), repositoryBlockData.getHeight()); - assertArrayEquals(archivedBlockData.getSignature(), repositoryBlockData.getSignature()); - assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); - assertArrayEquals(archivedBlockData.getMinterSignature(), repositoryBlockData.getMinterSignature()); - assertEquals(archivedBlockData.getATCount(), repositoryBlockData.getATCount()); - assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); - assertArrayEquals(archivedBlockData.getReference(), repositoryBlockData.getReference()); - assertEquals(archivedBlockData.getTimestamp(), repositoryBlockData.getTimestamp()); - assertEquals(archivedBlockData.getATFees(), repositoryBlockData.getATFees()); - assertEquals(archivedBlockData.getTotalFees(), repositoryBlockData.getTotalFees()); - assertEquals(archivedBlockData.getTransactionCount(), repositoryBlockData.getTransactionCount()); - assertArrayEquals(archivedBlockData.getTransactionsSignature(), repositoryBlockData.getTransactionsSignature()); - - if (testHeight != 2) { - assertArrayEquals(archivedAtStateData.getStateHash(), repositoryAtStateData.getStateHash()); - } - } - - // Check block 10 (unarchived) - BlockArchiveReader reader = BlockArchiveReader.getInstance(); - BlockTransformation blockInfo = reader.fetchBlockAtHeight(10); - assertNull(blockInfo); - - } - - } - - @Test - public void testArchiveAndPrune() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { - - // Deploy an AT so that we have AT state data - PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); - byte[] creationBytes = AtUtils.buildSimpleAT(); - long fundingAmount = 1_00000000L; - AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); - - // Mint some blocks so that we are able to archive them later - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - } - - // Assume 900 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); - repository.getATRepository().setAtTrimHeight(901); - - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - assertEquals(900, maximumArchiveHeight); - - // Write blocks 2-900 to the archive - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - - // Make sure that the archive contains the correct number of blocks - assertEquals(900 - 1, writer.getWrittenCount()); - - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(901); - repository.saveChanges(); - assertEquals(901, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - - // Ensure the SQL repository contains blocks 2 and 900... - assertNotNull(repository.getBlockRepository().fromHeight(2)); - assertNotNull(repository.getBlockRepository().fromHeight(900)); - - // Prune all the archived blocks - int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 900); - assertEquals(900-1, numBlocksPruned); - repository.getBlockRepository().setBlockPruneHeight(901); - - // Prune the AT states for the archived blocks - repository.getATRepository().rebuildLatestAtStates(900); - repository.saveChanges(); - int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 900); - assertEquals(900-2, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state - repository.getATRepository().setAtPruneHeight(901); - - // Now ensure the SQL repository is missing blocks 2 and 900... - assertNull(repository.getBlockRepository().fromHeight(2)); - assertNull(repository.getBlockRepository().fromHeight(900)); - - // ... but it's not missing blocks 1 and 901 (we don't prune the genesis block) - assertNotNull(repository.getBlockRepository().fromHeight(1)); - assertNotNull(repository.getBlockRepository().fromHeight(901)); - - // Validate the latest block height in the repository - assertEquals(1002, (int) repository.getBlockRepository().getLastBlock().getHeight()); - - } - } - - @Test - public void testTrimArchivePruneAndOrphan() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { - - // Deploy an AT so that we have AT state data - PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); - byte[] creationBytes = AtUtils.buildSimpleAT(); - long fundingAmount = 1_00000000L; - AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); - - // Mint some blocks so that we are able to archive them later - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - } - - // Make sure that block 500 has full AT state data and data hash - List block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); - ATStateData atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); - assertNotNull(atStatesData.getStateHash()); - assertNotNull(atStatesData.getStateData()); - - // Trim the first 500 blocks - repository.getBlockRepository().trimOldOnlineAccountsSignatures(0, 500); - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(501); - repository.getATRepository().rebuildLatestAtStates(500); - repository.getATRepository().trimAtStates(0, 500, 1000); - repository.getATRepository().setAtTrimHeight(501); - - // Now block 499 should only have the AT state data hash - List block499AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(499); - atStatesData = repository.getATRepository().getATStateAtHeight(block499AtStatesData.get(0).getATAddress(), 499); - assertNotNull(atStatesData.getStateHash()); - assertNull(atStatesData.getStateData()); - - // ... but block 500 should have the full data (due to being retained as the "latest" AT state in the trimmed range - block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); - atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); - assertNotNull(atStatesData.getStateHash()); - assertNotNull(atStatesData.getStateData()); - - // ... and block 501 should also have the full data - List block501AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(501); - atStatesData = repository.getATRepository().getATStateAtHeight(block501AtStatesData.get(0).getATAddress(), 501); - assertNotNull(atStatesData.getStateHash()); - assertNotNull(atStatesData.getStateData()); - - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - assertEquals(500, maximumArchiveHeight); - - BlockData block3DataPreArchive = repository.getBlockRepository().fromHeight(3); - - // Write blocks 2-500 to the archive - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - - // Make sure that the archive contains the correct number of blocks - assertEquals(500 - 1, writer.getWrittenCount()); // -1 for the genesis block - - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(500 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - - // Ensure the SQL repository contains blocks 2 and 500... - assertNotNull(repository.getBlockRepository().fromHeight(2)); - assertNotNull(repository.getBlockRepository().fromHeight(500)); - - // Prune all the archived blocks - int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 500); - assertEquals(500-1, numBlocksPruned); - repository.getBlockRepository().setBlockPruneHeight(501); - - // Prune the AT states for the archived blocks - repository.getATRepository().rebuildLatestAtStates(500); - repository.saveChanges(); - int numATStatesPruned = repository.getATRepository().pruneAtStates(2, 500); - assertEquals(498, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state - repository.getATRepository().setAtPruneHeight(501); - - // Now ensure the SQL repository is missing blocks 2 and 500... - assertNull(repository.getBlockRepository().fromHeight(2)); - assertNull(repository.getBlockRepository().fromHeight(500)); - - // ... but it's not missing blocks 1 and 501 (we don't prune the genesis block) - assertNotNull(repository.getBlockRepository().fromHeight(1)); - assertNotNull(repository.getBlockRepository().fromHeight(501)); - - // Validate the latest block height in the repository - assertEquals(1002, (int) repository.getBlockRepository().getLastBlock().getHeight()); - - // Now orphan some unarchived blocks. - BlockUtils.orphanBlocks(repository, 500); - assertEquals(502, (int) repository.getBlockRepository().getLastBlock().getHeight()); - - // We're close to the lower limit of the SQL database now, so - // we need to import some blocks from the archive - BlockArchiveUtils.importFromArchive(401, 500, repository); - - // Ensure the SQL repository now contains block 401 but not 400... - assertNotNull(repository.getBlockRepository().fromHeight(401)); - assertNull(repository.getBlockRepository().fromHeight(400)); - - // Import the remaining 399 blocks - BlockArchiveUtils.importFromArchive(2, 400, repository); - - // Verify that block 3 matches the original - BlockData block3DataPostArchive = repository.getBlockRepository().fromHeight(3); - assertArrayEquals(block3DataPreArchive.getSignature(), block3DataPostArchive.getSignature()); - assertEquals(block3DataPreArchive.getHeight(), block3DataPostArchive.getHeight()); - - // Orphan 1 more block, which should be the last one that is possible to be orphaned - BlockUtils.orphanBlocks(repository, 1); - - // Orphan another block, which should fail - Exception exception = null; - try { - BlockUtils.orphanBlocks(repository, 1); - } catch (DataException e) { - exception = e; - } - - // Ensure that a DataException is thrown because there is no more AT states data available - assertNotNull(exception); - assertEquals(DataException.class, exception.getClass()); - - // FUTURE: we may be able to retain unique AT states when trimming, to avoid this exception - // and allow orphaning back through blocks with trimmed AT states. - + public void testTrimArchivePruneAndOrphan() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { + + System.out.println("Starting testTrimArchivePruneAndOrphan"); + + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + byte[] creationBytes = AtUtils.buildSimpleAT(); + long fundingAmount = 1_00000000L; + AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + System.out.println("AT deployed successfully."); + + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); + + // Make sure that block 500 has full AT state data and data hash + System.out.println("Verifying block 500 AT state data..."); + List block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); + ATStateData atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); + assertNotNull(atStatesData.getStateHash()); + assertNotNull(atStatesData.getStateData()); + System.out.println("Block 500 AT state data verified."); + + // Trim the first 500 blocks + System.out.println("Trimming first 500 blocks..."); + repository.getBlockRepository().trimOldOnlineAccountsSignatures(0, 500); + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(501); + repository.getATRepository().rebuildLatestAtStates(500); + repository.getATRepository().trimAtStates(0, 500, 1000); + repository.getATRepository().setAtTrimHeight(501); + System.out.println("Trimming completed."); + + // Now block 499 should only have the AT state data hash + System.out.println("Checking block 499 AT state data..."); + List block499AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(499); + atStatesData = repository.getATRepository().getATStateAtHeight(block499AtStatesData.get(0).getATAddress(), 499); + assertNotNull(atStatesData.getStateHash()); + assertNull(atStatesData.getStateData()); + System.out.println("Block 499 AT state data contains only state hash as expected."); + + // ... but block 500 should have the full data (due to being retained as the "latest" AT state in the trimmed range + System.out.println("Verifying block 500 AT state data again..."); + block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); + atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); + assertNotNull(atStatesData.getStateHash()); + assertNotNull(atStatesData.getStateData()); + System.out.println("Block 500 AT state data contains full data."); + + // ... and block 501 should also have the full data + System.out.println("Verifying block 501 AT state data..."); + List block501AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(501); + atStatesData = repository.getATRepository().getATStateAtHeight(block501AtStatesData.get(0).getATAddress(), 501); + assertNotNull(atStatesData.getStateHash()); + assertNotNull(atStatesData.getStateData()); + System.out.println("Block 501 AT state data contains full data."); + + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height determined (Expected 500): " + maximumArchiveHeight); + assertEquals(500, maximumArchiveHeight); + + BlockData block3DataPreArchive = repository.getBlockRepository().fromHeight(3); + + // Write blocks 2-500 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + + // Make sure that the archive contains the correct number of blocks + System.out.println("Number of blocks written to archive (Expected 499): " + writer.getWrittenCount()); + assertEquals(500 - 1, writer.getWrittenCount()); // -1 for the genesis block + + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(500 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + System.out.println("Block archive height updated to: " + (500 - 1)); + + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + + // Ensure the SQL repository contains blocks 2 and 500... + System.out.println("Verifying that blocks 2 and 500 exist in the repository..."); + assertNotNull(repository.getBlockRepository().fromHeight(2)); + assertNotNull(repository.getBlockRepository().fromHeight(500)); + System.out.println("Blocks 2 and 500 are present in the repository."); + + // Prune all the archived blocks + System.out.println("Pruning blocks 2 to 500..."); + int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 500); + System.out.println("Number of blocks pruned (Expected 499): " + numBlocksPruned); + assertEquals(500-1, numBlocksPruned); + repository.getBlockRepository().setBlockPruneHeight(501); + + // Prune the AT states for the archived blocks + System.out.println("Pruning AT states up to height 500..."); + repository.getATRepository().rebuildLatestAtStates(500); + repository.saveChanges(); + int numATStatesPruned = repository.getATRepository().pruneAtStates(2, 500); + System.out.println("Number of AT states pruned (Expected 498): " + numATStatesPruned); + assertEquals(498, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state + repository.getATRepository().setAtPruneHeight(501); + + // Now ensure the SQL repository is missing blocks 2 and 500... + System.out.println("Verifying that blocks 2 and 500 have been pruned..."); + assertNull(repository.getBlockRepository().fromHeight(2)); + assertNull(repository.getBlockRepository().fromHeight(500)); + System.out.println("Blocks 2 and 500 have been successfully pruned."); + + // ... but it's not missing blocks 1 and 501 (we don't prune the genesis block) + System.out.println("Verifying that blocks 1 and 501 still exist..."); + assertNotNull(repository.getBlockRepository().fromHeight(1)); + assertNotNull(repository.getBlockRepository().fromHeight(501)); + System.out.println("Blocks 1 and 501 are present in the repository."); + + // Validate the latest block height in the repository + int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); + assertEquals(1002, lastBlockHeight); + + // Now orphan some unarchived blocks. + System.out.println("Orphaning 500 blocks..."); + BlockUtils.orphanBlocks(repository, 500); + int currentLastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("New last block height after orphaning (Expected 502): " + currentLastBlockHeight); + assertEquals(502, currentLastBlockHeight); + + // We're close to the lower limit of the SQL database now, so + // we need to import some blocks from the archive + System.out.println("Importing blocks 401 to 500 from the archive..."); + BlockArchiveUtils.importFromArchive(401, 500, repository); + + // Ensure the SQL repository now contains block 401 but not 400... + System.out.println("Verifying that block 401 exists and block 400 does not..."); + assertNotNull(repository.getBlockRepository().fromHeight(401)); + assertNull(repository.getBlockRepository().fromHeight(400)); + System.out.println("Block 401 exists, block 400 does not."); + + // Import the remaining 399 blocks + System.out.println("Importing blocks 2 to 400 from the archive..."); + BlockArchiveUtils.importFromArchive(2, 400, repository); + + // Verify that block 3 matches the original + System.out.println("Verifying that block 3 matches the original data..."); + BlockData block3DataPostArchive = repository.getBlockRepository().fromHeight(3); + assertArrayEquals(block3DataPreArchive.getSignature(), block3DataPostArchive.getSignature()); + assertEquals(block3DataPreArchive.getHeight(), block3DataPostArchive.getHeight()); + System.out.println("Block 3 data matches the original."); + + // Orphan 1 more block, which should be the last one that is possible to be orphaned + System.out.println("Orphaning 1 more block..."); + BlockUtils.orphanBlocks(repository, 1); + System.out.println("Orphaned 1 block successfully."); + + // Orphan another block, which should fail + System.out.println("Attempting to orphan another block, which should fail..."); + Exception exception = null; + try { + BlockUtils.orphanBlocks(repository, 1); + } catch (DataException e) { + exception = e; + System.out.println("Caught expected DataException: " + e.getMessage()); + } + + // Ensure that a DataException is thrown because there is no more AT states data available + assertNotNull(exception); + assertEquals(DataException.class, exception.getClass()); + System.out.println("DataException confirmed due to lack of AT states data."); + + // FUTURE: we may be able to retain unique AT states when trimming, to avoid this exception + // and allow orphaning back through blocks with trimmed AT states. + + System.out.println("testTrimArchivePruneAndOrphan completed successfully."); } } @@ -478,32 +611,44 @@ public class BlockArchiveV1Tests extends Common { * In these cases we disable archiving and pruning as this index is a * very essential component in these processes. */ - @Test - public void testMissingAtStatesHeightIndex() throws DataException, SQLException { - try (final HSQLDBRepository repository = (HSQLDBRepository) RepositoryManager.getRepository()) { + @Test + public void testMissingAtStatesHeightIndex() throws DataException, SQLException { + try (final HSQLDBRepository repository = (HSQLDBRepository) RepositoryManager.getRepository()) { - // Firstly check that we're able to prune or archive when the index exists - assertTrue(repository.getATRepository().hasAtStatesHeightIndex()); - assertTrue(RepositoryManager.canArchiveOrPrune()); + System.out.println("Starting testMissingAtStatesHeightIndex"); - // Delete the index - repository.prepareStatement("DROP INDEX ATSTATESHEIGHTINDEX").execute(); + // Firstly check that we're able to prune or archive when the index exists + System.out.println("Checking existence of ATStatesHeightIndex..."); + assertTrue(repository.getATRepository().hasAtStatesHeightIndex()); + assertTrue(RepositoryManager.canArchiveOrPrune()); + System.out.println("ATStatesHeightIndex exists. Archiving and pruning are possible."); - // Ensure check that we're unable to prune or archive when the index doesn't exist - assertFalse(repository.getATRepository().hasAtStatesHeightIndex()); - assertFalse(RepositoryManager.canArchiveOrPrune()); - } - } + // Delete the index + System.out.println("Dropping ATStatesHeightIndex..."); + repository.prepareStatement("DROP INDEX ATSTATESHEIGHTINDEX").execute(); + System.out.println("ATStatesHeightIndex dropped."); + + // Ensure check that we're unable to prune or archive when the index doesn't exist + System.out.println("Verifying that ATStatesHeightIndex no longer exists..."); + assertFalse(repository.getATRepository().hasAtStatesHeightIndex()); + assertFalse(RepositoryManager.canArchiveOrPrune()); + System.out.println("ATStatesHeightIndex does not exist. Archiving and pruning are disabled."); + + System.out.println("testMissingAtStatesHeightIndex completed successfully."); + } + } - private void deleteArchiveDirectory() { - // Delete archive directory if exists - Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toAbsolutePath(); - try { - FileUtils.deleteDirectory(archivePath.toFile()); - } catch (IOException e) { - - } - } + private void deleteArchiveDirectory() { + // Delete archive directory if exists + Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toAbsolutePath(); + try { + FileUtils.deleteDirectory(archivePath.toFile()); + System.out.println("Deleted archive directory at: " + archivePath); + } catch (IOException e) { + + System.out.println("Failed to delete archive directory: " + e.getMessage()); + } + } } diff --git a/src/test/java/org/qortal/test/BlockArchiveV2Tests.java b/src/test/java/org/qortal/test/BlockArchiveV2Tests.java index 3b1d12d3..784ac3d3 100644 --- a/src/test/java/org/qortal/test/BlockArchiveV2Tests.java +++ b/src/test/java/org/qortal/test/BlockArchiveV2Tests.java @@ -34,471 +34,625 @@ import static org.junit.Assert.*; public class BlockArchiveV2Tests extends Common { - @Before - public void beforeTest() throws DataException, IllegalAccessException { - Common.useSettings("test-settings-v2-block-archive.json"); - NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset()); - this.deleteArchiveDirectory(); + @Before + public void beforeTest() throws DataException, IllegalAccessException { + Common.useSettings("test-settings-v2-block-archive.json"); + NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset()); + this.deleteArchiveDirectory(); - // Set default archive version to 2, so that archive builds in these tests use V2 - FieldUtils.writeField(Settings.getInstance(), "defaultArchiveVersion", 2, true); - } + // Set default archive version to 2, so that archive builds in these tests use V2 + FieldUtils.writeField(Settings.getInstance(), "defaultArchiveVersion", 2, true); + } - @After - public void afterTest() throws DataException { - this.deleteArchiveDirectory(); - } + @After + public void afterTest() throws DataException { + this.deleteArchiveDirectory(); + } - @Test - public void testWriter() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { + @Test + public void testWriter() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { - // Mint some blocks so that we are able to archive them later - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - } + System.out.println("Starting testWriter"); - // 900 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); - repository.getATRepository().setAtTrimHeight(901); + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - assertEquals(900, maximumArchiveHeight); + // 900 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); + repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); - // Write blocks 2-900 to the archive - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); + assertEquals(900, maximumArchiveHeight); - // Make sure that the archive contains the correct number of blocks - assertEquals(900 - 1, writer.getWrittenCount()); + // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); + assertEquals(900 - 1, writer.getWrittenCount()); - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - } - } + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - @Test - public void testWriterAndReader() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - // Mint some blocks so that we are able to archive them later - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - } + System.out.println("testWriter completed successfully."); + } + } - // 900 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); - repository.getATRepository().setAtTrimHeight(901); + @Test + public void testWriterAndReader() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - assertEquals(900, maximumArchiveHeight); + System.out.println("Starting testWriterAndReader"); - // Write blocks 2-900 to the archive - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); - // Make sure that the archive contains the correct number of blocks - assertEquals(900 - 1, writer.getWrittenCount()); + // 900 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); + repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); + assertEquals(900, maximumArchiveHeight); - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); + // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - // Read block 2 from the archive - BlockArchiveReader reader = BlockArchiveReader.getInstance(); - BlockTransformation block2Info = reader.fetchBlockAtHeight(2); - BlockData block2ArchiveData = block2Info.getBlockData(); + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); + assertEquals(900 - 1, writer.getWrittenCount()); - // Read block 2 from the repository - BlockData block2RepositoryData = repository.getBlockRepository().fromHeight(2); + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - // Ensure the values match - assertEquals(block2ArchiveData.getHeight(), block2RepositoryData.getHeight()); - assertArrayEquals(block2ArchiveData.getSignature(), block2RepositoryData.getSignature()); + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - // Test some values in the archive - assertEquals(1, block2ArchiveData.getOnlineAccountsCount()); + // Read block 2 from the archive + System.out.println("Reading block 2 from the archive..."); + BlockArchiveReader reader = BlockArchiveReader.getInstance(); + BlockTransformation block2Info = reader.fetchBlockAtHeight(2); + BlockData block2ArchiveData = block2Info.getBlockData(); - // Read block 900 from the archive - BlockTransformation block900Info = reader.fetchBlockAtHeight(900); - BlockData block900ArchiveData = block900Info.getBlockData(); + // Read block 2 from the repository + BlockData block2RepositoryData = repository.getBlockRepository().fromHeight(2); - // Read block 900 from the repository - BlockData block900RepositoryData = repository.getBlockRepository().fromHeight(900); + // Ensure the values match + System.out.println("Comparing block 2 data..."); + assertEquals(block2ArchiveData.getHeight(), block2RepositoryData.getHeight()); + assertArrayEquals(block2ArchiveData.getSignature(), block2RepositoryData.getSignature()); - // Ensure the values match - assertEquals(block900ArchiveData.getHeight(), block900RepositoryData.getHeight()); - assertArrayEquals(block900ArchiveData.getSignature(), block900RepositoryData.getSignature()); + // Test some values in the archive + assertEquals(1, block2ArchiveData.getOnlineAccountsCount()); - // Test some values in the archive - assertEquals(1, block900ArchiveData.getOnlineAccountsCount()); + // Read block 900 from the archive + System.out.println("Reading block 900 from the archive..."); + BlockTransformation block900Info = reader.fetchBlockAtHeight(900); + BlockData block900ArchiveData = block900Info.getBlockData(); - } - } + // Read block 900 from the repository + BlockData block900RepositoryData = repository.getBlockRepository().fromHeight(900); - @Test - public void testArchivedAtStates() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { + // Ensure the values match + System.out.println("Comparing block 900 data..."); + assertEquals(block900ArchiveData.getHeight(), block900RepositoryData.getHeight()); + assertArrayEquals(block900ArchiveData.getSignature(), block900RepositoryData.getSignature()); - // Deploy an AT so that we have AT state data - PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); - byte[] creationBytes = AtUtils.buildSimpleAT(); - long fundingAmount = 1_00000000L; - DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); - String atAddress = deployAtTransaction.getATAccount().getAddress(); + // Test some values in the archive + assertEquals(1, block900ArchiveData.getOnlineAccountsCount()); - // Mint some blocks so that we are able to archive them later - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - } + System.out.println("testWriterAndReader completed successfully."); + } + } - // 9 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(10); - repository.getATRepository().setAtTrimHeight(10); + @Test + public void testArchivedAtStates() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { - // Check the max archive height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - assertEquals(9, maximumArchiveHeight); + System.out.println("Starting testArchivedAtStates"); - // Write blocks 2-9 to the archive - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + byte[] creationBytes = AtUtils.buildSimpleAT(); + long fundingAmount = 1_00000000L; + DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + String atAddress = deployAtTransaction.getATAccount().getAddress(); + System.out.println("AT deployed at address: " + atAddress); - // Make sure that the archive contains the correct number of blocks - assertEquals(9 - 1, writer.getWrittenCount()); + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(9 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + // 9 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(10); + repository.getATRepository().setAtTrimHeight(10); + System.out.println("Set trim heights to 10."); - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); + // Check the max archive height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 9): " + maximumArchiveHeight); + assertEquals(9, maximumArchiveHeight); - // Check blocks 3-9 - for (Integer testHeight = 2; testHeight <= 9; testHeight++) { + // Write blocks 2-9 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - // Read a block from the archive - BlockArchiveReader reader = BlockArchiveReader.getInstance(); - BlockTransformation blockInfo = reader.fetchBlockAtHeight(testHeight); - BlockData archivedBlockData = blockInfo.getBlockData(); - byte[] archivedAtStateHash = blockInfo.getAtStatesHash(); - List archivedTransactions = blockInfo.getTransactions(); + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 8)"); + assertEquals(9 - 1, writer.getWrittenCount()); - // Read the same block from the repository - BlockData repositoryBlockData = repository.getBlockRepository().fromHeight(testHeight); - ATStateData repositoryAtStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight); + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(9 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + System.out.println("Block archive height updated to: " + (9 - 1)); - // Ensure the repository has full AT state data - assertNotNull(repositoryAtStateData.getStateHash()); - assertNotNull(repositoryAtStateData.getStateData()); + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - // Check the archived AT state - if (testHeight == 2) { - assertEquals(1, archivedTransactions.size()); - assertEquals(Transaction.TransactionType.DEPLOY_AT, archivedTransactions.get(0).getType()); - } + // Check blocks 3-9 + System.out.println("Checking blocks 2 to 9..."); + for (Integer testHeight = 2; testHeight <= 9; testHeight++) { + + System.out.println("Reading block " + testHeight + " from the archive..."); + // Read a block from the archive + BlockArchiveReader reader = BlockArchiveReader.getInstance(); + BlockTransformation blockInfo = reader.fetchBlockAtHeight(testHeight); + BlockData archivedBlockData = blockInfo.getBlockData(); + byte[] archivedAtStateHash = blockInfo.getAtStatesHash(); + List archivedTransactions = blockInfo.getTransactions(); + + // Read the same block from the repository + BlockData repositoryBlockData = repository.getBlockRepository().fromHeight(testHeight); + ATStateData repositoryAtStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight); + + // Ensure the repository has full AT state data + assertNotNull(repositoryAtStateData.getStateHash()); + assertNotNull(repositoryAtStateData.getStateData()); + + // Check the archived AT state + if (testHeight == 2) { + System.out.println("Checking block " + testHeight + " AT state data (expected transactions)..."); + assertEquals(1, archivedTransactions.size()); + assertEquals(Transaction.TransactionType.DEPLOY_AT, archivedTransactions.get(0).getType()); + } else { - // Blocks 3+ shouldn't have any transactions - assertTrue(archivedTransactions.isEmpty()); - } + System.out.println("Checking block " + testHeight + " AT state data (no transactions expected)..."); + // Blocks 3+ shouldn't have any transactions + assertTrue(archivedTransactions.isEmpty()); + } - // Ensure the archive has the AT states hash - assertNotNull(archivedAtStateHash); + // Ensure the archive has the AT states hash + System.out.println("Checking block " + testHeight + " AT states hash..."); + assertNotNull(archivedAtStateHash); - // Also check the online accounts count and height - assertEquals(1, archivedBlockData.getOnlineAccountsCount()); - assertEquals(testHeight, archivedBlockData.getHeight()); + // Also check the online accounts count and height + assertEquals(1, archivedBlockData.getOnlineAccountsCount()); + assertEquals(testHeight, archivedBlockData.getHeight()); - // Ensure the values match - assertEquals(archivedBlockData.getHeight(), repositoryBlockData.getHeight()); - assertArrayEquals(archivedBlockData.getSignature(), repositoryBlockData.getSignature()); - assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); - assertArrayEquals(archivedBlockData.getMinterSignature(), repositoryBlockData.getMinterSignature()); - assertEquals(archivedBlockData.getATCount(), repositoryBlockData.getATCount()); - assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); - assertArrayEquals(archivedBlockData.getReference(), repositoryBlockData.getReference()); - assertEquals(archivedBlockData.getTimestamp(), repositoryBlockData.getTimestamp()); - assertEquals(archivedBlockData.getATFees(), repositoryBlockData.getATFees()); - assertEquals(archivedBlockData.getTotalFees(), repositoryBlockData.getTotalFees()); - assertEquals(archivedBlockData.getTransactionCount(), repositoryBlockData.getTransactionCount()); - assertArrayEquals(archivedBlockData.getTransactionsSignature(), repositoryBlockData.getTransactionsSignature()); + // Ensure the values match + System.out.println("Comparing block " + testHeight + " data..."); + assertEquals(archivedBlockData.getHeight(), repositoryBlockData.getHeight()); + assertArrayEquals(archivedBlockData.getSignature(), repositoryBlockData.getSignature()); + assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); + assertArrayEquals(archivedBlockData.getMinterSignature(), repositoryBlockData.getMinterSignature()); + assertEquals(archivedBlockData.getATCount(), repositoryBlockData.getATCount()); + assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); + assertArrayEquals(archivedBlockData.getReference(), repositoryBlockData.getReference()); + assertEquals(archivedBlockData.getTimestamp(), repositoryBlockData.getTimestamp()); + assertEquals(archivedBlockData.getATFees(), repositoryBlockData.getATFees()); + assertEquals(archivedBlockData.getTotalFees(), repositoryBlockData.getTotalFees()); + assertEquals(archivedBlockData.getTransactionCount(), repositoryBlockData.getTransactionCount()); + assertArrayEquals(archivedBlockData.getTransactionsSignature(), repositoryBlockData.getTransactionsSignature()); - // TODO: build atStatesHash and compare against value in archive - } + // TODO: build atStatesHash and compare against value in archive + } - // Check block 10 (unarchived) - BlockArchiveReader reader = BlockArchiveReader.getInstance(); - BlockTransformation blockInfo = reader.fetchBlockAtHeight(10); - assertNull(blockInfo); + // Check block 10 (unarchived) + System.out.println("Checking block 10 (should not be in archive)..."); + BlockArchiveReader reader = BlockArchiveReader.getInstance(); + BlockTransformation blockInfo = reader.fetchBlockAtHeight(10); + assertNull(blockInfo); - } + System.out.println("testArchivedAtStates completed successfully."); + } - } + } - @Test - public void testArchiveAndPrune() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { + @Test + public void testArchiveAndPrune() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { - // Deploy an AT so that we have AT state data - PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); - byte[] creationBytes = AtUtils.buildSimpleAT(); - long fundingAmount = 1_00000000L; - AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + System.out.println("Starting testArchiveAndPrune"); - // Mint some blocks so that we are able to archive them later - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - } + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + byte[] creationBytes = AtUtils.buildSimpleAT(); + long fundingAmount = 1_00000000L; + AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + System.out.println("AT deployed successfully."); - // Assume 900 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); - repository.getATRepository().setAtTrimHeight(901); + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - assertEquals(900, maximumArchiveHeight); + // Assume 900 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); + repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); - // Write blocks 2-900 to the archive - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); + assertEquals(900, maximumArchiveHeight); - // Make sure that the archive contains the correct number of blocks - assertEquals(900 - 1, writer.getWrittenCount()); + // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(901); - repository.saveChanges(); - assertEquals(901, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); + assertEquals(900 - 1, writer.getWrittenCount()); - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(901); + repository.saveChanges(); + assertEquals(901, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - // Ensure the SQL repository contains blocks 2 and 900... - assertNotNull(repository.getBlockRepository().fromHeight(2)); - assertNotNull(repository.getBlockRepository().fromHeight(900)); + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - // Prune all the archived blocks - int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 900); - assertEquals(900-1, numBlocksPruned); - repository.getBlockRepository().setBlockPruneHeight(901); + // Ensure the SQL repository contains blocks 2 and 900... + System.out.println("Verifying that blocks 2 and 900 exist in the repository..."); + assertNotNull(repository.getBlockRepository().fromHeight(2)); + assertNotNull(repository.getBlockRepository().fromHeight(900)); + System.out.println("Blocks 2 and 900 are present in the repository."); - // Prune the AT states for the archived blocks - repository.getATRepository().rebuildLatestAtStates(900); - repository.saveChanges(); - int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 900); - assertEquals(900-2, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state - repository.getATRepository().setAtPruneHeight(901); + // Prune all the archived blocks + System.out.println("Pruning blocks 2 to 900..."); + int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 900); + System.out.println("Number of blocks pruned (Expected 899): " + numBlocksPruned); + assertEquals(900-1, numBlocksPruned); + repository.getBlockRepository().setBlockPruneHeight(901); - // Now ensure the SQL repository is missing blocks 2 and 900... - assertNull(repository.getBlockRepository().fromHeight(2)); - assertNull(repository.getBlockRepository().fromHeight(900)); + // Prune the AT states for the archived blocks + System.out.println("Pruning AT states up to height 900..."); + repository.getATRepository().rebuildLatestAtStates(900); + repository.saveChanges(); + int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 900); + System.out.println("Number of AT states pruned (Expected 898): " + numATStatesPruned); + assertEquals(900-2, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state + repository.getATRepository().setAtPruneHeight(901); - // ... but it's not missing blocks 1 and 901 (we don't prune the genesis block) - assertNotNull(repository.getBlockRepository().fromHeight(1)); - assertNotNull(repository.getBlockRepository().fromHeight(901)); + // Now ensure the SQL repository is missing blocks 2 and 900... + System.out.println("Verifying that blocks 2 and 900 have been pruned..."); + assertNull(repository.getBlockRepository().fromHeight(2)); + assertNull(repository.getBlockRepository().fromHeight(900)); + System.out.println("Blocks 2 and 900 have been successfully pruned."); - // Validate the latest block height in the repository - assertEquals(1002, (int) repository.getBlockRepository().getLastBlock().getHeight()); + // ... but it's not missing blocks 1 and 901 (we don't prune the genesis block) + System.out.println("Verifying that blocks 1 and 901 still exist..."); + assertNotNull(repository.getBlockRepository().fromHeight(1)); + assertNotNull(repository.getBlockRepository().fromHeight(901)); + System.out.println("Blocks 1 and 901 are present in the repository."); - } - } + // Validate the latest block height in the repository + int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); + assertEquals(1002, lastBlockHeight); - @Test - public void testTrimArchivePruneAndOrphan() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("testArchiveAndPrune completed successfully."); + } + } - // Deploy an AT so that we have AT state data - PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); - byte[] creationBytes = AtUtils.buildSimpleAT(); - long fundingAmount = 1_00000000L; - AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + @Test + public void testTrimArchivePruneAndOrphan() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { - // Mint some blocks so that we are able to archive them later - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - } + System.out.println("Starting testTrimArchivePruneAndOrphan"); - // Make sure that block 500 has full AT state data and data hash - List block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); - ATStateData atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); - assertNotNull(atStatesData.getStateHash()); - assertNotNull(atStatesData.getStateData()); + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + byte[] creationBytes = AtUtils.buildSimpleAT(); + long fundingAmount = 1_00000000L; + AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + System.out.println("AT deployed successfully."); - // Trim the first 500 blocks - repository.getBlockRepository().trimOldOnlineAccountsSignatures(0, 500); - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(501); - repository.getATRepository().rebuildLatestAtStates(500); - repository.getATRepository().trimAtStates(0, 500, 1000); - repository.getATRepository().setAtTrimHeight(501); + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); - // Now block 499 should only have the AT state data hash - List block499AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(499); - atStatesData = repository.getATRepository().getATStateAtHeight(block499AtStatesData.get(0).getATAddress(), 499); - assertNotNull(atStatesData.getStateHash()); - assertNull(atStatesData.getStateData()); + // Make sure that block 500 has full AT state data and data hash + System.out.println("Verifying block 500 AT state data..."); + List block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); + ATStateData atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); + assertNotNull(atStatesData.getStateHash()); + assertNotNull(atStatesData.getStateData()); + System.out.println("Block 500 AT state data verified."); - // ... but block 500 should have the full data (due to being retained as the "latest" AT state in the trimmed range - block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); - atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); - assertNotNull(atStatesData.getStateHash()); - assertNotNull(atStatesData.getStateData()); + // Trim the first 500 blocks + System.out.println("Trimming first 500 blocks..."); + repository.getBlockRepository().trimOldOnlineAccountsSignatures(0, 500); + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(501); + repository.getATRepository().rebuildLatestAtStates(500); + repository.getATRepository().trimAtStates(0, 500, 1000); + repository.getATRepository().setAtTrimHeight(501); + System.out.println("Trimming completed."); - // ... and block 501 should also have the full data - List block501AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(501); - atStatesData = repository.getATRepository().getATStateAtHeight(block501AtStatesData.get(0).getATAddress(), 501); - assertNotNull(atStatesData.getStateHash()); - assertNotNull(atStatesData.getStateData()); + // Now block 499 should only have the AT state data hash + System.out.println("Checking block 499 AT state data..."); + List block499AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(499); + atStatesData = repository.getATRepository().getATStateAtHeight(block499AtStatesData.get(0).getATAddress(), 499); + assertNotNull(atStatesData.getStateHash()); + assertNull(atStatesData.getStateData()); + System.out.println("Block 499 AT state data contains only state hash as expected."); - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - assertEquals(500, maximumArchiveHeight); + // ... but block 500 should have the full data (due to being retained as the "latest" AT state in the trimmed range + System.out.println("Verifying block 500 AT state data again..."); + block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); + atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); + assertNotNull(atStatesData.getStateHash()); + assertNotNull(atStatesData.getStateData()); + System.out.println("Block 500 AT state data contains full data."); - BlockData block3DataPreArchive = repository.getBlockRepository().fromHeight(3); + // ... and block 501 should also have the full data + System.out.println("Verifying block 501 AT state data..."); + List block501AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(501); + atStatesData = repository.getATRepository().getATStateAtHeight(block501AtStatesData.get(0).getATAddress(), 501); + assertNotNull(atStatesData.getStateHash()); + assertNotNull(atStatesData.getStateData()); + System.out.println("Block 501 AT state data contains full data."); - // Write blocks 2-500 to the archive - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height determined (Expected 500): " + maximumArchiveHeight); + assertEquals(500, maximumArchiveHeight); - // Make sure that the archive contains the correct number of blocks - assertEquals(500 - 1, writer.getWrittenCount()); // -1 for the genesis block + BlockData block3DataPreArchive = repository.getBlockRepository().fromHeight(3); - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(500 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + // Write blocks 2-500 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); + // Make sure that the archive contains the correct number of blocks + System.out.println("Number of blocks written to archive (Expected 499): " + writer.getWrittenCount()); + assertEquals(500 - 1, writer.getWrittenCount()); // -1 for the genesis block - // Ensure the SQL repository contains blocks 2 and 500... - assertNotNull(repository.getBlockRepository().fromHeight(2)); - assertNotNull(repository.getBlockRepository().fromHeight(500)); + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(500 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + System.out.println("Block archive height updated to: " + (500 - 1)); - // Prune all the archived blocks - int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 500); - assertEquals(500-1, numBlocksPruned); - repository.getBlockRepository().setBlockPruneHeight(501); + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - // Prune the AT states for the archived blocks - repository.getATRepository().rebuildLatestAtStates(500); - repository.saveChanges(); - int numATStatesPruned = repository.getATRepository().pruneAtStates(2, 500); - assertEquals(498, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state - repository.getATRepository().setAtPruneHeight(501); + // Ensure the SQL repository contains blocks 2 and 500... + System.out.println("Verifying that blocks 2 and 500 exist in the repository..."); + assertNotNull(repository.getBlockRepository().fromHeight(2)); + assertNotNull(repository.getBlockRepository().fromHeight(500)); + System.out.println("Blocks 2 and 500 are present in the repository."); - // Now ensure the SQL repository is missing blocks 2 and 500... - assertNull(repository.getBlockRepository().fromHeight(2)); - assertNull(repository.getBlockRepository().fromHeight(500)); + // Prune all the archived blocks + System.out.println("Pruning blocks 2 to 500..."); + int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 500); + System.out.println("Number of blocks pruned (Expected 499): " + numBlocksPruned); + assertEquals(500-1, numBlocksPruned); + repository.getBlockRepository().setBlockPruneHeight(501); - // ... but it's not missing blocks 1 and 501 (we don't prune the genesis block) - assertNotNull(repository.getBlockRepository().fromHeight(1)); - assertNotNull(repository.getBlockRepository().fromHeight(501)); + // Prune the AT states for the archived blocks + System.out.println("Pruning AT states up to height 500..."); + repository.getATRepository().rebuildLatestAtStates(500); + repository.saveChanges(); + int numATStatesPruned = repository.getATRepository().pruneAtStates(2, 500); + System.out.println("Number of AT states pruned (Expected 498): " + numATStatesPruned); + assertEquals(498, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state + repository.getATRepository().setAtPruneHeight(501); - // Validate the latest block height in the repository - assertEquals(1002, (int) repository.getBlockRepository().getLastBlock().getHeight()); + // Now ensure the SQL repository is missing blocks 2 and 500... + System.out.println("Verifying that blocks 2 and 500 have been pruned..."); + assertNull(repository.getBlockRepository().fromHeight(2)); + assertNull(repository.getBlockRepository().fromHeight(500)); + System.out.println("Blocks 2 and 500 have been successfully pruned."); - // Now orphan some unarchived blocks. - BlockUtils.orphanBlocks(repository, 500); - assertEquals(502, (int) repository.getBlockRepository().getLastBlock().getHeight()); + // ... but it's not missing blocks 1 and 501 (we don't prune the genesis block) + System.out.println("Verifying that blocks 1 and 501 still exist..."); + assertNotNull(repository.getBlockRepository().fromHeight(1)); + assertNotNull(repository.getBlockRepository().fromHeight(501)); + System.out.println("Blocks 1 and 501 are present in the repository."); - // We're close to the lower limit of the SQL database now, so - // we need to import some blocks from the archive - BlockArchiveUtils.importFromArchive(401, 500, repository); + // Validate the latest block height in the repository + int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); + assertEquals(1002, lastBlockHeight); - // Ensure the SQL repository now contains block 401 but not 400... - assertNotNull(repository.getBlockRepository().fromHeight(401)); - assertNull(repository.getBlockRepository().fromHeight(400)); + // Now orphan some unarchived blocks. + System.out.println("Orphaning 500 blocks..."); + BlockUtils.orphanBlocks(repository, 500); + int currentLastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("New last block height after orphaning (Expected 502): " + currentLastBlockHeight); + assertEquals(502, currentLastBlockHeight); - // Import the remaining 399 blocks - BlockArchiveUtils.importFromArchive(2, 400, repository); + // We're close to the lower limit of the SQL database now, so + // we need to import some blocks from the archive + System.out.println("Importing blocks 401 to 500 from the archive..."); + BlockArchiveUtils.importFromArchive(401, 500, repository); - // Verify that block 3 matches the original - BlockData block3DataPostArchive = repository.getBlockRepository().fromHeight(3); - assertArrayEquals(block3DataPreArchive.getSignature(), block3DataPostArchive.getSignature()); - assertEquals(block3DataPreArchive.getHeight(), block3DataPostArchive.getHeight()); + // Ensure the SQL repository now contains block 401 but not 400... + System.out.println("Verifying that block 401 exists and block 400 does not..."); + assertNotNull(repository.getBlockRepository().fromHeight(401)); + assertNull(repository.getBlockRepository().fromHeight(400)); + System.out.println("Block 401 exists, block 400 does not."); - // Orphan 2 more block, which should be the last one that is possible to be orphaned + // Import the remaining 399 blocks + System.out.println("Importing blocks 2 to 400 from the archive..."); + BlockArchiveUtils.importFromArchive(2, 400, repository); + + // Verify that block 3 matches the original + System.out.println("Verifying that block 3 matches the original data..."); + BlockData block3DataPostArchive = repository.getBlockRepository().fromHeight(3); + assertArrayEquals(block3DataPreArchive.getSignature(), block3DataPostArchive.getSignature()); + assertEquals(block3DataPreArchive.getHeight(), block3DataPostArchive.getHeight()); + System.out.println("Block 3 data matches the original."); + + // Orphan 2 more block, which should be the last one that is possible to be orphaned // TODO: figure out why this is 1 block more than in the equivalent block archive V1 test - BlockUtils.orphanBlocks(repository, 2); + System.out.println("Orphaning 2 more blocks..."); + BlockUtils.orphanBlocks(repository, 2); + System.out.println("Orphaned 2 blocks successfully."); - // Orphan another block, which should fail - Exception exception = null; - try { - BlockUtils.orphanBlocks(repository, 1); - } catch (DataException e) { - exception = e; - } + // Orphan another block, which should fail + System.out.println("Attempting to orphan another block, which should fail..."); + Exception exception = null; + try { + BlockUtils.orphanBlocks(repository, 1); + } catch (DataException e) { + exception = e; + System.out.println("Caught expected DataException: " + e.getMessage()); + } - // Ensure that a DataException is thrown because there is no more AT states data available - assertNotNull(exception); - assertEquals(DataException.class, exception.getClass()); + // Ensure that a DataException is thrown because there is no more AT states data available + assertNotNull(exception); + assertEquals(DataException.class, exception.getClass()); + System.out.println("DataException confirmed due to lack of AT states data."); - // FUTURE: we may be able to retain unique AT states when trimming, to avoid this exception - // and allow orphaning back through blocks with trimmed AT states. + // FUTURE: we may be able to retain unique AT states when trimming, to avoid this exception + // and allow orphaning back through blocks with trimmed AT states. - } - } + System.out.println("testTrimArchivePruneAndOrphan completed successfully."); + } + } - /** - * Many nodes are missing an ATStatesHeightIndex due to an earlier bug - * In these cases we disable archiving and pruning as this index is a - * very essential component in these processes. - */ - @Test - public void testMissingAtStatesHeightIndex() throws DataException, SQLException { - try (final HSQLDBRepository repository = (HSQLDBRepository) RepositoryManager.getRepository()) { + /** + * Many nodes are missing an ATStatesHeightIndex due to an earlier bug + * In these cases we disable archiving and pruning as this index is a + * very essential component in these processes. + */ + @Test + public void testMissingAtStatesHeightIndex() throws DataException, SQLException { + try (final HSQLDBRepository repository = (HSQLDBRepository) RepositoryManager.getRepository()) { - // Firstly check that we're able to prune or archive when the index exists - assertTrue(repository.getATRepository().hasAtStatesHeightIndex()); - assertTrue(RepositoryManager.canArchiveOrPrune()); + System.out.println("Starting testMissingAtStatesHeightIndex"); - // Delete the index - repository.prepareStatement("DROP INDEX ATSTATESHEIGHTINDEX").execute(); + // Firstly check that we're able to prune or archive when the index exists + System.out.println("Checking existence of ATStatesHeightIndex..."); + assertTrue(repository.getATRepository().hasAtStatesHeightIndex()); + assertTrue(RepositoryManager.canArchiveOrPrune()); + System.out.println("ATStatesHeightIndex exists. Archiving and pruning are possible."); - // Ensure check that we're unable to prune or archive when the index doesn't exist - assertFalse(repository.getATRepository().hasAtStatesHeightIndex()); - assertFalse(RepositoryManager.canArchiveOrPrune()); - } - } + // Delete the index + System.out.println("Dropping ATStatesHeightIndex..."); + repository.prepareStatement("DROP INDEX ATSTATESHEIGHTINDEX").execute(); + System.out.println("ATStatesHeightIndex dropped."); + + // Ensure check that we're unable to prune or archive when the index doesn't exist + System.out.println("Verifying that ATStatesHeightIndex no longer exists..."); + assertFalse(repository.getATRepository().hasAtStatesHeightIndex()); + assertFalse(RepositoryManager.canArchiveOrPrune()); + System.out.println("ATStatesHeightIndex does not exist. Archiving and pruning are disabled."); + + System.out.println("testMissingAtStatesHeightIndex completed successfully."); + } + } - private void deleteArchiveDirectory() { - // Delete archive directory if exists - Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toAbsolutePath(); - try { - FileUtils.deleteDirectory(archivePath.toFile()); - } catch (IOException e) { + private void deleteArchiveDirectory() { + // Delete archive directory if exists + Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toAbsolutePath(); + try { + FileUtils.deleteDirectory(archivePath.toFile()); + System.out.println("Deleted archive directory at: " + archivePath); + } catch (IOException e) { - } - } + System.out.println("Failed to delete archive directory: " + e.getMessage()); + } + } } From 3d83a79014966423798a9257eacdc849c8804499 Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Wed, 13 Nov 2024 06:28:23 -0500 Subject: [PATCH 03/16] Fix whitespace only --- .../org/qortal/test/BlockArchiveV1Tests.java | 1128 ++++++++--------- .../org/qortal/test/BlockArchiveV2Tests.java | 1014 +++++++-------- 2 files changed, 1071 insertions(+), 1071 deletions(-) diff --git a/src/test/java/org/qortal/test/BlockArchiveV1Tests.java b/src/test/java/org/qortal/test/BlockArchiveV1Tests.java index 60eefa8e..2cf8ef79 100644 --- a/src/test/java/org/qortal/test/BlockArchiveV1Tests.java +++ b/src/test/java/org/qortal/test/BlockArchiveV1Tests.java @@ -34,574 +34,574 @@ import static org.junit.Assert.*; public class BlockArchiveV1Tests extends Common { - @Before - public void beforeTest() throws DataException, IllegalAccessException { - Common.useSettings("test-settings-v2-block-archive.json"); - NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset()); - this.deleteArchiveDirectory(); + @Before + public void beforeTest() throws DataException, IllegalAccessException { + Common.useSettings("test-settings-v2-block-archive.json"); + NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset()); + this.deleteArchiveDirectory(); - // Set default archive version to 1, so that archive builds in these tests use V2 - FieldUtils.writeField(Settings.getInstance(), "defaultArchiveVersion", 1, true); - } + // Set default archive version to 1, so that archive builds in these tests use V2 + FieldUtils.writeField(Settings.getInstance(), "defaultArchiveVersion", 1, true); + } - @After - public void afterTest() throws DataException { - this.deleteArchiveDirectory(); - } + @After + public void afterTest() throws DataException { + this.deleteArchiveDirectory(); + } - @Test - public void testWriter() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { - - System.out.println("Starting testWriter"); - - // Mint some blocks so that we are able to archive them later - System.out.println("Minting 1000 blocks..."); - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - // Log every 100 blocks - if ((i + 1) % 100 == 0) { - System.out.println("Minted block " + (i + 1)); - } - } - System.out.println("Finished minting blocks."); - - // 900 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); - repository.getATRepository().setAtTrimHeight(901); - System.out.println("Set trim heights to 901."); - - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); - assertEquals(900, maximumArchiveHeight); - - // Write blocks 2-900 to the archive - System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - System.out.println("Finished writing blocks to archive. Result: " + result); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - - // Make sure that the archive contains the correct number of blocks - System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); - assertEquals(900 - 1, writer.getWrittenCount()); - - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - - System.out.println("testWriter completed successfully."); - } - } - - @Test - public void testWriterAndReader() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { - - System.out.println("Starting testWriterAndReader"); - - // Mint some blocks so that we are able to archive them later - System.out.println("Minting 1000 blocks..."); - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - // Log every 100 blocks - if ((i + 1) % 100 == 0) { - System.out.println("Minted block " + (i + 1)); - } - } - System.out.println("Finished minting blocks."); - - // 900 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); - repository.getATRepository().setAtTrimHeight(901); - System.out.println("Set trim heights to 901."); - - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); - assertEquals(900, maximumArchiveHeight); - - // Write blocks 2-900 to the archive - System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - System.out.println("Finished writing blocks to archive. Result: " + result); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - - // Make sure that the archive contains the correct number of blocks - System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); - assertEquals(900 - 1, writer.getWrittenCount()); - - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - - // Read block 2 from the archive - System.out.println("Reading block 2 from the archive..."); - BlockArchiveReader reader = BlockArchiveReader.getInstance(); - BlockTransformation block2Info = reader.fetchBlockAtHeight(2); - BlockData block2ArchiveData = block2Info.getBlockData(); - - // Read block 2 from the repository - BlockData block2RepositoryData = repository.getBlockRepository().fromHeight(2); - - // Ensure the values match - System.out.println("Comparing block 2 data..."); - assertEquals(block2ArchiveData.getHeight(), block2RepositoryData.getHeight()); - assertArrayEquals(block2ArchiveData.getSignature(), block2RepositoryData.getSignature()); - - // Test some values in the archive - assertEquals(1, block2ArchiveData.getOnlineAccountsCount()); - - // Read block 900 from the archive - System.out.println("Reading block 900 from the archive..."); - BlockTransformation block900Info = reader.fetchBlockAtHeight(900); - BlockData block900ArchiveData = block900Info.getBlockData(); - - // Read block 900 from the repository - BlockData block900RepositoryData = repository.getBlockRepository().fromHeight(900); - - // Ensure the values match - System.out.println("Comparing block 900 data..."); - assertEquals(block900ArchiveData.getHeight(), block900RepositoryData.getHeight()); - assertArrayEquals(block900ArchiveData.getSignature(), block900RepositoryData.getSignature()); - - // Test some values in the archive - assertEquals(1, block900ArchiveData.getOnlineAccountsCount()); - - System.out.println("testWriterAndReader completed successfully."); - } - } - - @Test - public void testArchivedAtStates() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { - - System.out.println("Starting testArchivedAtStates"); - - // Deploy an AT so that we have AT state data - System.out.println("Deploying AT..."); - PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); - byte[] creationBytes = AtUtils.buildSimpleAT(); - long fundingAmount = 1_00000000L; - DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); - String atAddress = deployAtTransaction.getATAccount().getAddress(); - System.out.println("AT deployed at address: " + atAddress); - - // Mint some blocks so that we are able to archive them later - System.out.println("Minting 1000 blocks..."); - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - // Log every 100 blocks - if ((i + 1) % 100 == 0) { - System.out.println("Minted block " + (i + 1)); - } - } - System.out.println("Finished minting blocks."); - - // 9 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(10); - repository.getATRepository().setAtTrimHeight(10); - System.out.println("Set trim heights to 10."); - - // Check the max archive height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - System.out.println("Maximum archive height (Expected 9): " + maximumArchiveHeight); - assertEquals(9, maximumArchiveHeight); - - // Write blocks 2-9 to the archive - System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - System.out.println("Finished writing blocks to archive. Result: " + result); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - - // Make sure that the archive contains the correct number of blocks - System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 8)"); - assertEquals(9 - 1, writer.getWrittenCount()); - - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(9 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - - // Check blocks 3-9 - System.out.println("Checking blocks 3 to 9..."); - for (Integer testHeight = 2; testHeight <= 9; testHeight++) { - - System.out.println("Reading block " + testHeight + " from the archive..."); - // Read a block from the archive - BlockArchiveReader reader = BlockArchiveReader.getInstance(); - BlockTransformation blockInfo = reader.fetchBlockAtHeight(testHeight); - BlockData archivedBlockData = blockInfo.getBlockData(); - ATStateData archivedAtStateData = blockInfo.getAtStates().isEmpty() ? null : blockInfo.getAtStates().get(0); - List archivedTransactions = blockInfo.getTransactions(); - - // Read the same block from the repository - BlockData repositoryBlockData = repository.getBlockRepository().fromHeight(testHeight); - ATStateData repositoryAtStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight); - - // Ensure the repository has full AT state data - assertNotNull(repositoryAtStateData.getStateHash()); - assertNotNull(repositoryAtStateData.getStateData()); - - // Check the archived AT state - if (testHeight == 2) { - System.out.println("Checking block " + testHeight + " AT state data (expected null)..."); - // Block 2 won't have an AT state hash because it's initial (and has the DEPLOY_AT in the same block) - assertNull(archivedAtStateData); - - assertEquals(1, archivedTransactions.size()); - assertEquals(Transaction.TransactionType.DEPLOY_AT, archivedTransactions.get(0).getType()); - } - else { - System.out.println("Checking block " + testHeight + " AT state data..."); - // For blocks 3+, ensure the archive has the AT state data, but not the hashes - assertNotNull(archivedAtStateData.getStateHash()); - assertNull(archivedAtStateData.getStateData()); - - // They also shouldn't have any transactions - assertTrue(archivedTransactions.isEmpty()); - } - - // Also check the online accounts count and height - assertEquals(1, archivedBlockData.getOnlineAccountsCount()); - assertEquals(testHeight, archivedBlockData.getHeight()); - - // Ensure the values match - assertEquals(archivedBlockData.getHeight(), repositoryBlockData.getHeight()); - assertArrayEquals(archivedBlockData.getSignature(), repositoryBlockData.getSignature()); - assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); - assertArrayEquals(archivedBlockData.getMinterSignature(), repositoryBlockData.getMinterSignature()); - assertEquals(archivedBlockData.getATCount(), repositoryBlockData.getATCount()); - assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); - assertArrayEquals(archivedBlockData.getReference(), repositoryBlockData.getReference()); - assertEquals(archivedBlockData.getTimestamp(), repositoryBlockData.getTimestamp()); - assertEquals(archivedBlockData.getATFees(), repositoryBlockData.getATFees()); - assertEquals(archivedBlockData.getTotalFees(), repositoryBlockData.getTotalFees()); - assertEquals(archivedBlockData.getTransactionCount(), repositoryBlockData.getTransactionCount()); - assertArrayEquals(archivedBlockData.getTransactionsSignature(), repositoryBlockData.getTransactionsSignature()); - - if (testHeight != 2) { - assertArrayEquals(archivedAtStateData.getStateHash(), repositoryAtStateData.getStateHash()); - } - } - - // Check block 10 (unarchived) - System.out.println("Checking block 10 (should not be in archive)..."); - BlockArchiveReader reader = BlockArchiveReader.getInstance(); - BlockTransformation blockInfo = reader.fetchBlockAtHeight(10); - assertNull(blockInfo); - - System.out.println("testArchivedAtStates completed successfully."); - } - - } - - @Test - public void testArchiveAndPrune() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { - - System.out.println("Starting testArchiveAndPrune"); - - // Deploy an AT so that we have AT state data - System.out.println("Deploying AT..."); - PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); - byte[] creationBytes = AtUtils.buildSimpleAT(); - long fundingAmount = 1_00000000L; - AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); - - // Mint some blocks so that we are able to archive them later - System.out.println("Minting 1000 blocks..."); - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - // Log every 100 blocks - if ((i + 1) % 100 == 0) { - System.out.println("Minted block " + (i + 1)); - } - } - System.out.println("Finished minting blocks."); - - // Assume 900 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); - repository.getATRepository().setAtTrimHeight(901); - System.out.println("Set trim heights to 901."); - - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); - assertEquals(900, maximumArchiveHeight); - - // Write blocks 2-900 to the archive - System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - System.out.println("Finished writing blocks to archive. Result: " + result); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - - // Make sure that the archive contains the correct number of blocks - System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); - assertEquals(900 - 1, writer.getWrittenCount()); - - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(901); - repository.saveChanges(); - assertEquals(901, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - - // Ensure the SQL repository contains blocks 2 and 900... - assertNotNull(repository.getBlockRepository().fromHeight(2)); - assertNotNull(repository.getBlockRepository().fromHeight(900)); - System.out.println("Blocks 2 and 900 exist in the repository."); - - // Prune all the archived blocks - System.out.println("Pruning blocks 2 to 900..."); - int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 900); - assertEquals(900-1, numBlocksPruned); - repository.getBlockRepository().setBlockPruneHeight(901); - - // Prune the AT states for the archived blocks - System.out.println("Pruning AT states up to height 900..."); - repository.getATRepository().rebuildLatestAtStates(900); - repository.saveChanges(); - int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 900); - assertEquals(900-2, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state - repository.getATRepository().setAtPruneHeight(901); - - // Now ensure the SQL repository is missing blocks 2 and 900... - assertNull(repository.getBlockRepository().fromHeight(2)); - assertNull(repository.getBlockRepository().fromHeight(900)); - System.out.println("Blocks 2 and 900 have been pruned from the repository."); - - // ... but it's not missing blocks 1 and 901 (we don't prune the genesis block) - assertNotNull(repository.getBlockRepository().fromHeight(1)); - assertNotNull(repository.getBlockRepository().fromHeight(901)); - System.out.println("Blocks 1 and 901 still exist in the repository."); - - // Validate the latest block height in the repository - int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); - System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); - assertEquals(1002, lastBlockHeight); - - System.out.println("testArchiveAndPrune completed successfully."); - } - } - @Test - public void testTrimArchivePruneAndOrphan() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { + public void testWriter() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { - System.out.println("Starting testTrimArchivePruneAndOrphan"); + System.out.println("Starting testWriter"); - // Deploy an AT so that we have AT state data - System.out.println("Deploying AT..."); - PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); - byte[] creationBytes = AtUtils.buildSimpleAT(); - long fundingAmount = 1_00000000L; - AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); - System.out.println("AT deployed successfully."); + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); - // Mint some blocks so that we are able to archive them later - System.out.println("Minting 1000 blocks..."); - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - // Log every 100 blocks - if ((i + 1) % 100 == 0) { - System.out.println("Minted block " + (i + 1)); - } - } - System.out.println("Finished minting blocks."); + // 900 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); + repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); - // Make sure that block 500 has full AT state data and data hash - System.out.println("Verifying block 500 AT state data..."); - List block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); - ATStateData atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); - assertNotNull(atStatesData.getStateHash()); - assertNotNull(atStatesData.getStateData()); - System.out.println("Block 500 AT state data verified."); + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); + assertEquals(900, maximumArchiveHeight); - // Trim the first 500 blocks - System.out.println("Trimming first 500 blocks..."); - repository.getBlockRepository().trimOldOnlineAccountsSignatures(0, 500); - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(501); - repository.getATRepository().rebuildLatestAtStates(500); - repository.getATRepository().trimAtStates(0, 500, 1000); - repository.getATRepository().setAtTrimHeight(501); - System.out.println("Trimming completed."); + // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - // Now block 499 should only have the AT state data hash - System.out.println("Checking block 499 AT state data..."); - List block499AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(499); - atStatesData = repository.getATRepository().getATStateAtHeight(block499AtStatesData.get(0).getATAddress(), 499); - assertNotNull(atStatesData.getStateHash()); - assertNull(atStatesData.getStateData()); - System.out.println("Block 499 AT state data contains only state hash as expected."); + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); + assertEquals(900 - 1, writer.getWrittenCount()); - // ... but block 500 should have the full data (due to being retained as the "latest" AT state in the trimmed range - System.out.println("Verifying block 500 AT state data again..."); - block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); - atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); - assertNotNull(atStatesData.getStateHash()); - assertNotNull(atStatesData.getStateData()); - System.out.println("Block 500 AT state data contains full data."); + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - // ... and block 501 should also have the full data - System.out.println("Verifying block 501 AT state data..."); - List block501AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(501); - atStatesData = repository.getATRepository().getATStateAtHeight(block501AtStatesData.get(0).getATAddress(), 501); - assertNotNull(atStatesData.getStateHash()); - assertNotNull(atStatesData.getStateData()); - System.out.println("Block 501 AT state data contains full data."); + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - System.out.println("Maximum archive height determined (Expected 500): " + maximumArchiveHeight); - assertEquals(500, maximumArchiveHeight); + System.out.println("testWriter completed successfully."); + } + } - BlockData block3DataPreArchive = repository.getBlockRepository().fromHeight(3); + @Test + public void testWriterAndReader() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { - // Write blocks 2-500 to the archive - System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - System.out.println("Finished writing blocks to archive. Result: " + result); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + System.out.println("Starting testWriterAndReader"); - // Make sure that the archive contains the correct number of blocks - System.out.println("Number of blocks written to archive (Expected 499): " + writer.getWrittenCount()); - assertEquals(500 - 1, writer.getWrittenCount()); // -1 for the genesis block + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(500 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - System.out.println("Block archive height updated to: " + (500 - 1)); + // 900 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); + repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); + assertEquals(900, maximumArchiveHeight); - // Ensure the SQL repository contains blocks 2 and 500... - System.out.println("Verifying that blocks 2 and 500 exist in the repository..."); - assertNotNull(repository.getBlockRepository().fromHeight(2)); - assertNotNull(repository.getBlockRepository().fromHeight(500)); - System.out.println("Blocks 2 and 500 are present in the repository."); + // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - // Prune all the archived blocks - System.out.println("Pruning blocks 2 to 500..."); - int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 500); - System.out.println("Number of blocks pruned (Expected 499): " + numBlocksPruned); - assertEquals(500-1, numBlocksPruned); - repository.getBlockRepository().setBlockPruneHeight(501); + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); + assertEquals(900 - 1, writer.getWrittenCount()); - // Prune the AT states for the archived blocks - System.out.println("Pruning AT states up to height 500..."); - repository.getATRepository().rebuildLatestAtStates(500); - repository.saveChanges(); - int numATStatesPruned = repository.getATRepository().pruneAtStates(2, 500); - System.out.println("Number of AT states pruned (Expected 498): " + numATStatesPruned); - assertEquals(498, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state - repository.getATRepository().setAtPruneHeight(501); + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - // Now ensure the SQL repository is missing blocks 2 and 500... - System.out.println("Verifying that blocks 2 and 500 have been pruned..."); - assertNull(repository.getBlockRepository().fromHeight(2)); - assertNull(repository.getBlockRepository().fromHeight(500)); - System.out.println("Blocks 2 and 500 have been successfully pruned."); + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - // ... but it's not missing blocks 1 and 501 (we don't prune the genesis block) - System.out.println("Verifying that blocks 1 and 501 still exist..."); - assertNotNull(repository.getBlockRepository().fromHeight(1)); - assertNotNull(repository.getBlockRepository().fromHeight(501)); - System.out.println("Blocks 1 and 501 are present in the repository."); + // Read block 2 from the archive + System.out.println("Reading block 2 from the archive..."); + BlockArchiveReader reader = BlockArchiveReader.getInstance(); + BlockTransformation block2Info = reader.fetchBlockAtHeight(2); + BlockData block2ArchiveData = block2Info.getBlockData(); - // Validate the latest block height in the repository - int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); - System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); - assertEquals(1002, lastBlockHeight); + // Read block 2 from the repository + BlockData block2RepositoryData = repository.getBlockRepository().fromHeight(2); - // Now orphan some unarchived blocks. - System.out.println("Orphaning 500 blocks..."); - BlockUtils.orphanBlocks(repository, 500); - int currentLastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); - System.out.println("New last block height after orphaning (Expected 502): " + currentLastBlockHeight); - assertEquals(502, currentLastBlockHeight); + // Ensure the values match + System.out.println("Comparing block 2 data..."); + assertEquals(block2ArchiveData.getHeight(), block2RepositoryData.getHeight()); + assertArrayEquals(block2ArchiveData.getSignature(), block2RepositoryData.getSignature()); - // We're close to the lower limit of the SQL database now, so - // we need to import some blocks from the archive - System.out.println("Importing blocks 401 to 500 from the archive..."); - BlockArchiveUtils.importFromArchive(401, 500, repository); + // Test some values in the archive + assertEquals(1, block2ArchiveData.getOnlineAccountsCount()); - // Ensure the SQL repository now contains block 401 but not 400... - System.out.println("Verifying that block 401 exists and block 400 does not..."); - assertNotNull(repository.getBlockRepository().fromHeight(401)); - assertNull(repository.getBlockRepository().fromHeight(400)); - System.out.println("Block 401 exists, block 400 does not."); + // Read block 900 from the archive + System.out.println("Reading block 900 from the archive..."); + BlockTransformation block900Info = reader.fetchBlockAtHeight(900); + BlockData block900ArchiveData = block900Info.getBlockData(); - // Import the remaining 399 blocks - System.out.println("Importing blocks 2 to 400 from the archive..."); - BlockArchiveUtils.importFromArchive(2, 400, repository); + // Read block 900 from the repository + BlockData block900RepositoryData = repository.getBlockRepository().fromHeight(900); - // Verify that block 3 matches the original - System.out.println("Verifying that block 3 matches the original data..."); - BlockData block3DataPostArchive = repository.getBlockRepository().fromHeight(3); - assertArrayEquals(block3DataPreArchive.getSignature(), block3DataPostArchive.getSignature()); - assertEquals(block3DataPreArchive.getHeight(), block3DataPostArchive.getHeight()); - System.out.println("Block 3 data matches the original."); + // Ensure the values match + System.out.println("Comparing block 900 data..."); + assertEquals(block900ArchiveData.getHeight(), block900RepositoryData.getHeight()); + assertArrayEquals(block900ArchiveData.getSignature(), block900RepositoryData.getSignature()); - // Orphan 1 more block, which should be the last one that is possible to be orphaned - System.out.println("Orphaning 1 more block..."); - BlockUtils.orphanBlocks(repository, 1); - System.out.println("Orphaned 1 block successfully."); + // Test some values in the archive + assertEquals(1, block900ArchiveData.getOnlineAccountsCount()); - // Orphan another block, which should fail - System.out.println("Attempting to orphan another block, which should fail..."); - Exception exception = null; - try { - BlockUtils.orphanBlocks(repository, 1); - } catch (DataException e) { - exception = e; - System.out.println("Caught expected DataException: " + e.getMessage()); - } + System.out.println("testWriterAndReader completed successfully."); + } + } - // Ensure that a DataException is thrown because there is no more AT states data available - assertNotNull(exception); - assertEquals(DataException.class, exception.getClass()); - System.out.println("DataException confirmed due to lack of AT states data."); + @Test + public void testArchivedAtStates() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { - // FUTURE: we may be able to retain unique AT states when trimming, to avoid this exception - // and allow orphaning back through blocks with trimmed AT states. + System.out.println("Starting testArchivedAtStates"); - System.out.println("testTrimArchivePruneAndOrphan completed successfully."); + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + byte[] creationBytes = AtUtils.buildSimpleAT(); + long fundingAmount = 1_00000000L; + DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + String atAddress = deployAtTransaction.getATAccount().getAddress(); + System.out.println("AT deployed at address: " + atAddress); + + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); + + // 9 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(10); + repository.getATRepository().setAtTrimHeight(10); + System.out.println("Set trim heights to 10."); + + // Check the max archive height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 9): " + maximumArchiveHeight); + assertEquals(9, maximumArchiveHeight); + + // Write blocks 2-9 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 8)"); + assertEquals(9 - 1, writer.getWrittenCount()); + + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(9 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + + // Check blocks 3-9 + System.out.println("Checking blocks 3 to 9..."); + for (Integer testHeight = 2; testHeight <= 9; testHeight++) { + + System.out.println("Reading block " + testHeight + " from the archive..."); + // Read a block from the archive + BlockArchiveReader reader = BlockArchiveReader.getInstance(); + BlockTransformation blockInfo = reader.fetchBlockAtHeight(testHeight); + BlockData archivedBlockData = blockInfo.getBlockData(); + ATStateData archivedAtStateData = blockInfo.getAtStates().isEmpty() ? null : blockInfo.getAtStates().get(0); + List archivedTransactions = blockInfo.getTransactions(); + + // Read the same block from the repository + BlockData repositoryBlockData = repository.getBlockRepository().fromHeight(testHeight); + ATStateData repositoryAtStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight); + + // Ensure the repository has full AT state data + assertNotNull(repositoryAtStateData.getStateHash()); + assertNotNull(repositoryAtStateData.getStateData()); + + // Check the archived AT state + if (testHeight == 2) { + System.out.println("Checking block " + testHeight + " AT state data (expected null)..."); + // Block 2 won't have an AT state hash because it's initial (and has the DEPLOY_AT in the same block) + assertNull(archivedAtStateData); + + assertEquals(1, archivedTransactions.size()); + assertEquals(Transaction.TransactionType.DEPLOY_AT, archivedTransactions.get(0).getType()); + } + else { + System.out.println("Checking block " + testHeight + " AT state data..."); + // For blocks 3+, ensure the archive has the AT state data, but not the hashes + assertNotNull(archivedAtStateData.getStateHash()); + assertNull(archivedAtStateData.getStateData()); + + // They also shouldn't have any transactions + assertTrue(archivedTransactions.isEmpty()); + } + + // Also check the online accounts count and height + assertEquals(1, archivedBlockData.getOnlineAccountsCount()); + assertEquals(testHeight, archivedBlockData.getHeight()); + + // Ensure the values match + assertEquals(archivedBlockData.getHeight(), repositoryBlockData.getHeight()); + assertArrayEquals(archivedBlockData.getSignature(), repositoryBlockData.getSignature()); + assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); + assertArrayEquals(archivedBlockData.getMinterSignature(), repositoryBlockData.getMinterSignature()); + assertEquals(archivedBlockData.getATCount(), repositoryBlockData.getATCount()); + assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); + assertArrayEquals(archivedBlockData.getReference(), repositoryBlockData.getReference()); + assertEquals(archivedBlockData.getTimestamp(), repositoryBlockData.getTimestamp()); + assertEquals(archivedBlockData.getATFees(), repositoryBlockData.getATFees()); + assertEquals(archivedBlockData.getTotalFees(), repositoryBlockData.getTotalFees()); + assertEquals(archivedBlockData.getTransactionCount(), repositoryBlockData.getTransactionCount()); + assertArrayEquals(archivedBlockData.getTransactionsSignature(), repositoryBlockData.getTransactionsSignature()); + + if (testHeight != 2) { + assertArrayEquals(archivedAtStateData.getStateHash(), repositoryAtStateData.getStateHash()); + } + } + + // Check block 10 (unarchived) + System.out.println("Checking block 10 (should not be in archive)..."); + BlockArchiveReader reader = BlockArchiveReader.getInstance(); + BlockTransformation blockInfo = reader.fetchBlockAtHeight(10); + assertNull(blockInfo); + + System.out.println("testArchivedAtStates completed successfully."); + } + + } + + @Test + public void testArchiveAndPrune() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { + + System.out.println("Starting testArchiveAndPrune"); + + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + byte[] creationBytes = AtUtils.buildSimpleAT(); + long fundingAmount = 1_00000000L; + AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); + + // Assume 900 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); + repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); + + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); + assertEquals(900, maximumArchiveHeight); + + // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); + assertEquals(900 - 1, writer.getWrittenCount()); + + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(901); + repository.saveChanges(); + assertEquals(901, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + + // Ensure the SQL repository contains blocks 2 and 900... + assertNotNull(repository.getBlockRepository().fromHeight(2)); + assertNotNull(repository.getBlockRepository().fromHeight(900)); + System.out.println("Blocks 2 and 900 exist in the repository."); + + // Prune all the archived blocks + System.out.println("Pruning blocks 2 to 900..."); + int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 900); + assertEquals(900-1, numBlocksPruned); + repository.getBlockRepository().setBlockPruneHeight(901); + + // Prune the AT states for the archived blocks + System.out.println("Pruning AT states up to height 900..."); + repository.getATRepository().rebuildLatestAtStates(900); + repository.saveChanges(); + int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 900); + assertEquals(900-2, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state + repository.getATRepository().setAtPruneHeight(901); + + // Now ensure the SQL repository is missing blocks 2 and 900... + assertNull(repository.getBlockRepository().fromHeight(2)); + assertNull(repository.getBlockRepository().fromHeight(900)); + System.out.println("Blocks 2 and 900 have been pruned from the repository."); + + // ... but it's not missing blocks 1 and 901 (we don't prune the genesis block) + assertNotNull(repository.getBlockRepository().fromHeight(1)); + assertNotNull(repository.getBlockRepository().fromHeight(901)); + System.out.println("Blocks 1 and 901 still exist in the repository."); + + // Validate the latest block height in the repository + int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); + assertEquals(1002, lastBlockHeight); + + System.out.println("testArchiveAndPrune completed successfully."); + } + } + + @Test + public void testTrimArchivePruneAndOrphan() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { + + System.out.println("Starting testTrimArchivePruneAndOrphan"); + + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + byte[] creationBytes = AtUtils.buildSimpleAT(); + long fundingAmount = 1_00000000L; + AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + System.out.println("AT deployed successfully."); + + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); + + // Make sure that block 500 has full AT state data and data hash + System.out.println("Verifying block 500 AT state data..."); + List block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); + ATStateData atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); + assertNotNull(atStatesData.getStateHash()); + assertNotNull(atStatesData.getStateData()); + System.out.println("Block 500 AT state data verified."); + + // Trim the first 500 blocks + System.out.println("Trimming first 500 blocks..."); + repository.getBlockRepository().trimOldOnlineAccountsSignatures(0, 500); + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(501); + repository.getATRepository().rebuildLatestAtStates(500); + repository.getATRepository().trimAtStates(0, 500, 1000); + repository.getATRepository().setAtTrimHeight(501); + System.out.println("Trimming completed."); + + // Now block 499 should only have the AT state data hash + System.out.println("Checking block 499 AT state data..."); + List block499AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(499); + atStatesData = repository.getATRepository().getATStateAtHeight(block499AtStatesData.get(0).getATAddress(), 499); + assertNotNull(atStatesData.getStateHash()); + assertNull(atStatesData.getStateData()); + System.out.println("Block 499 AT state data contains only state hash as expected."); + + // ... but block 500 should have the full data (due to being retained as the "latest" AT state in the trimmed range + System.out.println("Verifying block 500 AT state data again..."); + block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); + atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); + assertNotNull(atStatesData.getStateHash()); + assertNotNull(atStatesData.getStateData()); + System.out.println("Block 500 AT state data contains full data."); + + // ... and block 501 should also have the full data + System.out.println("Verifying block 501 AT state data..."); + List block501AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(501); + atStatesData = repository.getATRepository().getATStateAtHeight(block501AtStatesData.get(0).getATAddress(), 501); + assertNotNull(atStatesData.getStateHash()); + assertNotNull(atStatesData.getStateData()); + System.out.println("Block 501 AT state data contains full data."); + + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height determined (Expected 500): " + maximumArchiveHeight); + assertEquals(500, maximumArchiveHeight); + + BlockData block3DataPreArchive = repository.getBlockRepository().fromHeight(3); + + // Write blocks 2-500 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + + // Make sure that the archive contains the correct number of blocks + System.out.println("Number of blocks written to archive (Expected 499): " + writer.getWrittenCount()); + assertEquals(500 - 1, writer.getWrittenCount()); // -1 for the genesis block + + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(500 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + System.out.println("Block archive height updated to: " + (500 - 1)); + + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + + // Ensure the SQL repository contains blocks 2 and 500... + System.out.println("Verifying that blocks 2 and 500 exist in the repository..."); + assertNotNull(repository.getBlockRepository().fromHeight(2)); + assertNotNull(repository.getBlockRepository().fromHeight(500)); + System.out.println("Blocks 2 and 500 are present in the repository."); + + // Prune all the archived blocks + System.out.println("Pruning blocks 2 to 500..."); + int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 500); + System.out.println("Number of blocks pruned (Expected 499): " + numBlocksPruned); + assertEquals(500-1, numBlocksPruned); + repository.getBlockRepository().setBlockPruneHeight(501); + + // Prune the AT states for the archived blocks + System.out.println("Pruning AT states up to height 500..."); + repository.getATRepository().rebuildLatestAtStates(500); + repository.saveChanges(); + int numATStatesPruned = repository.getATRepository().pruneAtStates(2, 500); + System.out.println("Number of AT states pruned (Expected 498): " + numATStatesPruned); + assertEquals(498, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state + repository.getATRepository().setAtPruneHeight(501); + + // Now ensure the SQL repository is missing blocks 2 and 500... + System.out.println("Verifying that blocks 2 and 500 have been pruned..."); + assertNull(repository.getBlockRepository().fromHeight(2)); + assertNull(repository.getBlockRepository().fromHeight(500)); + System.out.println("Blocks 2 and 500 have been successfully pruned."); + + // ... but it's not missing blocks 1 and 501 (we don't prune the genesis block) + System.out.println("Verifying that blocks 1 and 501 still exist..."); + assertNotNull(repository.getBlockRepository().fromHeight(1)); + assertNotNull(repository.getBlockRepository().fromHeight(501)); + System.out.println("Blocks 1 and 501 are present in the repository."); + + // Validate the latest block height in the repository + int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); + assertEquals(1002, lastBlockHeight); + + // Now orphan some unarchived blocks. + System.out.println("Orphaning 500 blocks..."); + BlockUtils.orphanBlocks(repository, 500); + int currentLastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("New last block height after orphaning (Expected 502): " + currentLastBlockHeight); + assertEquals(502, currentLastBlockHeight); + + // We're close to the lower limit of the SQL database now, so + // we need to import some blocks from the archive + System.out.println("Importing blocks 401 to 500 from the archive..."); + BlockArchiveUtils.importFromArchive(401, 500, repository); + + // Ensure the SQL repository now contains block 401 but not 400... + System.out.println("Verifying that block 401 exists and block 400 does not..."); + assertNotNull(repository.getBlockRepository().fromHeight(401)); + assertNull(repository.getBlockRepository().fromHeight(400)); + System.out.println("Block 401 exists, block 400 does not."); + + // Import the remaining 399 blocks + System.out.println("Importing blocks 2 to 400 from the archive..."); + BlockArchiveUtils.importFromArchive(2, 400, repository); + + // Verify that block 3 matches the original + System.out.println("Verifying that block 3 matches the original data..."); + BlockData block3DataPostArchive = repository.getBlockRepository().fromHeight(3); + assertArrayEquals(block3DataPreArchive.getSignature(), block3DataPostArchive.getSignature()); + assertEquals(block3DataPreArchive.getHeight(), block3DataPostArchive.getHeight()); + System.out.println("Block 3 data matches the original."); + + // Orphan 1 more block, which should be the last one that is possible to be orphaned + System.out.println("Orphaning 1 more block..."); + BlockUtils.orphanBlocks(repository, 1); + System.out.println("Orphaned 1 block successfully."); + + // Orphan another block, which should fail + System.out.println("Attempting to orphan another block, which should fail..."); + Exception exception = null; + try { + BlockUtils.orphanBlocks(repository, 1); + } catch (DataException e) { + exception = e; + System.out.println("Caught expected DataException: " + e.getMessage()); + } + + // Ensure that a DataException is thrown because there is no more AT states data available + assertNotNull(exception); + assertEquals(DataException.class, exception.getClass()); + System.out.println("DataException confirmed due to lack of AT states data."); + + // FUTURE: we may be able to retain unique AT states when trimming, to avoid this exception + // and allow orphaning back through blocks with trimmed AT states. + + System.out.println("testTrimArchivePruneAndOrphan completed successfully."); } } @@ -611,44 +611,44 @@ public class BlockArchiveV1Tests extends Common { * In these cases we disable archiving and pruning as this index is a * very essential component in these processes. */ - @Test - public void testMissingAtStatesHeightIndex() throws DataException, SQLException { - try (final HSQLDBRepository repository = (HSQLDBRepository) RepositoryManager.getRepository()) { + @Test + public void testMissingAtStatesHeightIndex() throws DataException, SQLException { + try (final HSQLDBRepository repository = (HSQLDBRepository) RepositoryManager.getRepository()) { - System.out.println("Starting testMissingAtStatesHeightIndex"); + System.out.println("Starting testMissingAtStatesHeightIndex"); - // Firstly check that we're able to prune or archive when the index exists - System.out.println("Checking existence of ATStatesHeightIndex..."); - assertTrue(repository.getATRepository().hasAtStatesHeightIndex()); - assertTrue(RepositoryManager.canArchiveOrPrune()); - System.out.println("ATStatesHeightIndex exists. Archiving and pruning are possible."); + // Firstly check that we're able to prune or archive when the index exists + System.out.println("Checking existence of ATStatesHeightIndex..."); + assertTrue(repository.getATRepository().hasAtStatesHeightIndex()); + assertTrue(RepositoryManager.canArchiveOrPrune()); + System.out.println("ATStatesHeightIndex exists. Archiving and pruning are possible."); - // Delete the index - System.out.println("Dropping ATStatesHeightIndex..."); - repository.prepareStatement("DROP INDEX ATSTATESHEIGHTINDEX").execute(); - System.out.println("ATStatesHeightIndex dropped."); + // Delete the index + System.out.println("Dropping ATStatesHeightIndex..."); + repository.prepareStatement("DROP INDEX ATSTATESHEIGHTINDEX").execute(); + System.out.println("ATStatesHeightIndex dropped."); - // Ensure check that we're unable to prune or archive when the index doesn't exist - System.out.println("Verifying that ATStatesHeightIndex no longer exists..."); - assertFalse(repository.getATRepository().hasAtStatesHeightIndex()); - assertFalse(RepositoryManager.canArchiveOrPrune()); - System.out.println("ATStatesHeightIndex does not exist. Archiving and pruning are disabled."); + // Ensure check that we're unable to prune or archive when the index doesn't exist + System.out.println("Verifying that ATStatesHeightIndex no longer exists..."); + assertFalse(repository.getATRepository().hasAtStatesHeightIndex()); + assertFalse(RepositoryManager.canArchiveOrPrune()); + System.out.println("ATStatesHeightIndex does not exist. Archiving and pruning are disabled."); - System.out.println("testMissingAtStatesHeightIndex completed successfully."); - } - } + System.out.println("testMissingAtStatesHeightIndex completed successfully."); + } + } - private void deleteArchiveDirectory() { - // Delete archive directory if exists - Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toAbsolutePath(); - try { - FileUtils.deleteDirectory(archivePath.toFile()); - System.out.println("Deleted archive directory at: " + archivePath); - } catch (IOException e) { + private void deleteArchiveDirectory() { + // Delete archive directory if exists + Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toAbsolutePath(); + try { + FileUtils.deleteDirectory(archivePath.toFile()); + System.out.println("Deleted archive directory at: " + archivePath); + } catch (IOException e) { - System.out.println("Failed to delete archive directory: " + e.getMessage()); - } - } + System.out.println("Failed to delete archive directory: " + e.getMessage()); + } + } } diff --git a/src/test/java/org/qortal/test/BlockArchiveV2Tests.java b/src/test/java/org/qortal/test/BlockArchiveV2Tests.java index 784ac3d3..8ab02b40 100644 --- a/src/test/java/org/qortal/test/BlockArchiveV2Tests.java +++ b/src/test/java/org/qortal/test/BlockArchiveV2Tests.java @@ -34,625 +34,625 @@ import static org.junit.Assert.*; public class BlockArchiveV2Tests extends Common { - @Before - public void beforeTest() throws DataException, IllegalAccessException { - Common.useSettings("test-settings-v2-block-archive.json"); - NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset()); - this.deleteArchiveDirectory(); + @Before + public void beforeTest() throws DataException, IllegalAccessException { + Common.useSettings("test-settings-v2-block-archive.json"); + NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset()); + this.deleteArchiveDirectory(); - // Set default archive version to 2, so that archive builds in these tests use V2 - FieldUtils.writeField(Settings.getInstance(), "defaultArchiveVersion", 2, true); - } + // Set default archive version to 2, so that archive builds in these tests use V2 + FieldUtils.writeField(Settings.getInstance(), "defaultArchiveVersion", 2, true); + } - @After - public void afterTest() throws DataException { - this.deleteArchiveDirectory(); - } + @After + public void afterTest() throws DataException { + this.deleteArchiveDirectory(); + } - @Test - public void testWriter() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { + @Test + public void testWriter() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { - System.out.println("Starting testWriter"); + System.out.println("Starting testWriter"); - // Mint some blocks so that we are able to archive them later - System.out.println("Minting 1000 blocks..."); - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - // Log every 100 blocks - if ((i + 1) % 100 == 0) { - System.out.println("Minted block " + (i + 1)); - } - } - System.out.println("Finished minting blocks."); + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); - // 900 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); - repository.getATRepository().setAtTrimHeight(901); - System.out.println("Set trim heights to 901."); + // 900 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); + repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); - assertEquals(900, maximumArchiveHeight); + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); + assertEquals(900, maximumArchiveHeight); - // Write blocks 2-900 to the archive - System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - System.out.println("Finished writing blocks to archive. Result: " + result); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - // Make sure that the archive contains the correct number of blocks - System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); - assertEquals(900 - 1, writer.getWrittenCount()); + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); + assertEquals(900 - 1, writer.getWrittenCount()); - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - System.out.println("testWriter completed successfully."); - } - } + System.out.println("testWriter completed successfully."); + } + } - @Test - public void testWriterAndReader() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { + @Test + public void testWriterAndReader() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { - System.out.println("Starting testWriterAndReader"); + System.out.println("Starting testWriterAndReader"); - // Mint some blocks so that we are able to archive them later - System.out.println("Minting 1000 blocks..."); - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - // Log every 100 blocks - if ((i + 1) % 100 == 0) { - System.out.println("Minted block " + (i + 1)); - } - } - System.out.println("Finished minting blocks."); + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); - // 900 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); - repository.getATRepository().setAtTrimHeight(901); - System.out.println("Set trim heights to 901."); + // 900 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); + repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); - assertEquals(900, maximumArchiveHeight); + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); + assertEquals(900, maximumArchiveHeight); - // Write blocks 2-900 to the archive - System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - System.out.println("Finished writing blocks to archive. Result: " + result); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - // Make sure that the archive contains the correct number of blocks - System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); - assertEquals(900 - 1, writer.getWrittenCount()); + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); + assertEquals(900 - 1, writer.getWrittenCount()); - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - // Read block 2 from the archive - System.out.println("Reading block 2 from the archive..."); - BlockArchiveReader reader = BlockArchiveReader.getInstance(); - BlockTransformation block2Info = reader.fetchBlockAtHeight(2); - BlockData block2ArchiveData = block2Info.getBlockData(); + // Read block 2 from the archive + System.out.println("Reading block 2 from the archive..."); + BlockArchiveReader reader = BlockArchiveReader.getInstance(); + BlockTransformation block2Info = reader.fetchBlockAtHeight(2); + BlockData block2ArchiveData = block2Info.getBlockData(); - // Read block 2 from the repository - BlockData block2RepositoryData = repository.getBlockRepository().fromHeight(2); + // Read block 2 from the repository + BlockData block2RepositoryData = repository.getBlockRepository().fromHeight(2); - // Ensure the values match - System.out.println("Comparing block 2 data..."); - assertEquals(block2ArchiveData.getHeight(), block2RepositoryData.getHeight()); - assertArrayEquals(block2ArchiveData.getSignature(), block2RepositoryData.getSignature()); + // Ensure the values match + System.out.println("Comparing block 2 data..."); + assertEquals(block2ArchiveData.getHeight(), block2RepositoryData.getHeight()); + assertArrayEquals(block2ArchiveData.getSignature(), block2RepositoryData.getSignature()); - // Test some values in the archive - assertEquals(1, block2ArchiveData.getOnlineAccountsCount()); + // Test some values in the archive + assertEquals(1, block2ArchiveData.getOnlineAccountsCount()); - // Read block 900 from the archive - System.out.println("Reading block 900 from the archive..."); - BlockTransformation block900Info = reader.fetchBlockAtHeight(900); - BlockData block900ArchiveData = block900Info.getBlockData(); + // Read block 900 from the archive + System.out.println("Reading block 900 from the archive..."); + BlockTransformation block900Info = reader.fetchBlockAtHeight(900); + BlockData block900ArchiveData = block900Info.getBlockData(); - // Read block 900 from the repository - BlockData block900RepositoryData = repository.getBlockRepository().fromHeight(900); + // Read block 900 from the repository + BlockData block900RepositoryData = repository.getBlockRepository().fromHeight(900); - // Ensure the values match - System.out.println("Comparing block 900 data..."); - assertEquals(block900ArchiveData.getHeight(), block900RepositoryData.getHeight()); - assertArrayEquals(block900ArchiveData.getSignature(), block900RepositoryData.getSignature()); + // Ensure the values match + System.out.println("Comparing block 900 data..."); + assertEquals(block900ArchiveData.getHeight(), block900RepositoryData.getHeight()); + assertArrayEquals(block900ArchiveData.getSignature(), block900RepositoryData.getSignature()); - // Test some values in the archive - assertEquals(1, block900ArchiveData.getOnlineAccountsCount()); + // Test some values in the archive + assertEquals(1, block900ArchiveData.getOnlineAccountsCount()); - System.out.println("testWriterAndReader completed successfully."); - } - } + System.out.println("testWriterAndReader completed successfully."); + } + } - @Test - public void testArchivedAtStates() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { + @Test + public void testArchivedAtStates() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { - System.out.println("Starting testArchivedAtStates"); + System.out.println("Starting testArchivedAtStates"); - // Deploy an AT so that we have AT state data - System.out.println("Deploying AT..."); - PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); - byte[] creationBytes = AtUtils.buildSimpleAT(); - long fundingAmount = 1_00000000L; - DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); - String atAddress = deployAtTransaction.getATAccount().getAddress(); - System.out.println("AT deployed at address: " + atAddress); + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + byte[] creationBytes = AtUtils.buildSimpleAT(); + long fundingAmount = 1_00000000L; + DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + String atAddress = deployAtTransaction.getATAccount().getAddress(); + System.out.println("AT deployed at address: " + atAddress); - // Mint some blocks so that we are able to archive them later - System.out.println("Minting 1000 blocks..."); - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - // Log every 100 blocks - if ((i + 1) % 100 == 0) { - System.out.println("Minted block " + (i + 1)); - } - } - System.out.println("Finished minting blocks."); + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); - // 9 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(10); - repository.getATRepository().setAtTrimHeight(10); - System.out.println("Set trim heights to 10."); + // 9 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(10); + repository.getATRepository().setAtTrimHeight(10); + System.out.println("Set trim heights to 10."); - // Check the max archive height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - System.out.println("Maximum archive height (Expected 9): " + maximumArchiveHeight); - assertEquals(9, maximumArchiveHeight); + // Check the max archive height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 9): " + maximumArchiveHeight); + assertEquals(9, maximumArchiveHeight); - // Write blocks 2-9 to the archive - System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - System.out.println("Finished writing blocks to archive. Result: " + result); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + // Write blocks 2-9 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - // Make sure that the archive contains the correct number of blocks - System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 8)"); - assertEquals(9 - 1, writer.getWrittenCount()); + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 8)"); + assertEquals(9 - 1, writer.getWrittenCount()); - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(9 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - System.out.println("Block archive height updated to: " + (9 - 1)); + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(9 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + System.out.println("Block archive height updated to: " + (9 - 1)); - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - // Check blocks 3-9 - System.out.println("Checking blocks 2 to 9..."); - for (Integer testHeight = 2; testHeight <= 9; testHeight++) { + // Check blocks 3-9 + System.out.println("Checking blocks 2 to 9..."); + for (Integer testHeight = 2; testHeight <= 9; testHeight++) { - System.out.println("Reading block " + testHeight + " from the archive..."); - // Read a block from the archive - BlockArchiveReader reader = BlockArchiveReader.getInstance(); - BlockTransformation blockInfo = reader.fetchBlockAtHeight(testHeight); - BlockData archivedBlockData = blockInfo.getBlockData(); - byte[] archivedAtStateHash = blockInfo.getAtStatesHash(); - List archivedTransactions = blockInfo.getTransactions(); + System.out.println("Reading block " + testHeight + " from the archive..."); + // Read a block from the archive + BlockArchiveReader reader = BlockArchiveReader.getInstance(); + BlockTransformation blockInfo = reader.fetchBlockAtHeight(testHeight); + BlockData archivedBlockData = blockInfo.getBlockData(); + byte[] archivedAtStateHash = blockInfo.getAtStatesHash(); + List archivedTransactions = blockInfo.getTransactions(); - // Read the same block from the repository - BlockData repositoryBlockData = repository.getBlockRepository().fromHeight(testHeight); - ATStateData repositoryAtStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight); + // Read the same block from the repository + BlockData repositoryBlockData = repository.getBlockRepository().fromHeight(testHeight); + ATStateData repositoryAtStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight); - // Ensure the repository has full AT state data - assertNotNull(repositoryAtStateData.getStateHash()); - assertNotNull(repositoryAtStateData.getStateData()); + // Ensure the repository has full AT state data + assertNotNull(repositoryAtStateData.getStateHash()); + assertNotNull(repositoryAtStateData.getStateData()); - // Check the archived AT state - if (testHeight == 2) { - System.out.println("Checking block " + testHeight + " AT state data (expected transactions)..."); - assertEquals(1, archivedTransactions.size()); - assertEquals(Transaction.TransactionType.DEPLOY_AT, archivedTransactions.get(0).getType()); - } + // Check the archived AT state + if (testHeight == 2) { + System.out.println("Checking block " + testHeight + " AT state data (expected transactions)..."); + assertEquals(1, archivedTransactions.size()); + assertEquals(Transaction.TransactionType.DEPLOY_AT, archivedTransactions.get(0).getType()); + } else { - System.out.println("Checking block " + testHeight + " AT state data (no transactions expected)..."); - // Blocks 3+ shouldn't have any transactions - assertTrue(archivedTransactions.isEmpty()); - } + System.out.println("Checking block " + testHeight + " AT state data (no transactions expected)..."); + // Blocks 3+ shouldn't have any transactions + assertTrue(archivedTransactions.isEmpty()); + } - // Ensure the archive has the AT states hash - System.out.println("Checking block " + testHeight + " AT states hash..."); - assertNotNull(archivedAtStateHash); + // Ensure the archive has the AT states hash + System.out.println("Checking block " + testHeight + " AT states hash..."); + assertNotNull(archivedAtStateHash); - // Also check the online accounts count and height - assertEquals(1, archivedBlockData.getOnlineAccountsCount()); - assertEquals(testHeight, archivedBlockData.getHeight()); + // Also check the online accounts count and height + assertEquals(1, archivedBlockData.getOnlineAccountsCount()); + assertEquals(testHeight, archivedBlockData.getHeight()); - // Ensure the values match - System.out.println("Comparing block " + testHeight + " data..."); - assertEquals(archivedBlockData.getHeight(), repositoryBlockData.getHeight()); - assertArrayEquals(archivedBlockData.getSignature(), repositoryBlockData.getSignature()); - assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); - assertArrayEquals(archivedBlockData.getMinterSignature(), repositoryBlockData.getMinterSignature()); - assertEquals(archivedBlockData.getATCount(), repositoryBlockData.getATCount()); - assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); - assertArrayEquals(archivedBlockData.getReference(), repositoryBlockData.getReference()); - assertEquals(archivedBlockData.getTimestamp(), repositoryBlockData.getTimestamp()); - assertEquals(archivedBlockData.getATFees(), repositoryBlockData.getATFees()); - assertEquals(archivedBlockData.getTotalFees(), repositoryBlockData.getTotalFees()); - assertEquals(archivedBlockData.getTransactionCount(), repositoryBlockData.getTransactionCount()); - assertArrayEquals(archivedBlockData.getTransactionsSignature(), repositoryBlockData.getTransactionsSignature()); + // Ensure the values match + System.out.println("Comparing block " + testHeight + " data..."); + assertEquals(archivedBlockData.getHeight(), repositoryBlockData.getHeight()); + assertArrayEquals(archivedBlockData.getSignature(), repositoryBlockData.getSignature()); + assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); + assertArrayEquals(archivedBlockData.getMinterSignature(), repositoryBlockData.getMinterSignature()); + assertEquals(archivedBlockData.getATCount(), repositoryBlockData.getATCount()); + assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); + assertArrayEquals(archivedBlockData.getReference(), repositoryBlockData.getReference()); + assertEquals(archivedBlockData.getTimestamp(), repositoryBlockData.getTimestamp()); + assertEquals(archivedBlockData.getATFees(), repositoryBlockData.getATFees()); + assertEquals(archivedBlockData.getTotalFees(), repositoryBlockData.getTotalFees()); + assertEquals(archivedBlockData.getTransactionCount(), repositoryBlockData.getTransactionCount()); + assertArrayEquals(archivedBlockData.getTransactionsSignature(), repositoryBlockData.getTransactionsSignature()); - // TODO: build atStatesHash and compare against value in archive - } + // TODO: build atStatesHash and compare against value in archive + } - // Check block 10 (unarchived) - System.out.println("Checking block 10 (should not be in archive)..."); - BlockArchiveReader reader = BlockArchiveReader.getInstance(); - BlockTransformation blockInfo = reader.fetchBlockAtHeight(10); - assertNull(blockInfo); + // Check block 10 (unarchived) + System.out.println("Checking block 10 (should not be in archive)..."); + BlockArchiveReader reader = BlockArchiveReader.getInstance(); + BlockTransformation blockInfo = reader.fetchBlockAtHeight(10); + assertNull(blockInfo); - System.out.println("testArchivedAtStates completed successfully."); - } + System.out.println("testArchivedAtStates completed successfully."); + } - } + } - @Test - public void testArchiveAndPrune() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { + @Test + public void testArchiveAndPrune() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { - System.out.println("Starting testArchiveAndPrune"); + System.out.println("Starting testArchiveAndPrune"); - // Deploy an AT so that we have AT state data - System.out.println("Deploying AT..."); - PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); - byte[] creationBytes = AtUtils.buildSimpleAT(); - long fundingAmount = 1_00000000L; - AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); - System.out.println("AT deployed successfully."); + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + byte[] creationBytes = AtUtils.buildSimpleAT(); + long fundingAmount = 1_00000000L; + AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + System.out.println("AT deployed successfully."); - // Mint some blocks so that we are able to archive them later - System.out.println("Minting 1000 blocks..."); - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - // Log every 100 blocks - if ((i + 1) % 100 == 0) { - System.out.println("Minted block " + (i + 1)); - } - } - System.out.println("Finished minting blocks."); + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); - // Assume 900 blocks are trimmed (this specifies the first untrimmed height) - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); - repository.getATRepository().setAtTrimHeight(901); - System.out.println("Set trim heights to 901."); + // Assume 900 blocks are trimmed (this specifies the first untrimmed height) + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); + repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); - assertEquals(900, maximumArchiveHeight); + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); + assertEquals(900, maximumArchiveHeight); - // Write blocks 2-900 to the archive - System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - System.out.println("Finished writing blocks to archive. Result: " + result); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - // Make sure that the archive contains the correct number of blocks - System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); - assertEquals(900 - 1, writer.getWrittenCount()); + // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); + assertEquals(900 - 1, writer.getWrittenCount()); - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(901); - repository.saveChanges(); - assertEquals(901, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(901); + repository.saveChanges(); + assertEquals(901, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - // Ensure the SQL repository contains blocks 2 and 900... - System.out.println("Verifying that blocks 2 and 900 exist in the repository..."); - assertNotNull(repository.getBlockRepository().fromHeight(2)); - assertNotNull(repository.getBlockRepository().fromHeight(900)); - System.out.println("Blocks 2 and 900 are present in the repository."); + // Ensure the SQL repository contains blocks 2 and 900... + System.out.println("Verifying that blocks 2 and 900 exist in the repository..."); + assertNotNull(repository.getBlockRepository().fromHeight(2)); + assertNotNull(repository.getBlockRepository().fromHeight(900)); + System.out.println("Blocks 2 and 900 are present in the repository."); - // Prune all the archived blocks - System.out.println("Pruning blocks 2 to 900..."); - int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 900); - System.out.println("Number of blocks pruned (Expected 899): " + numBlocksPruned); - assertEquals(900-1, numBlocksPruned); - repository.getBlockRepository().setBlockPruneHeight(901); + // Prune all the archived blocks + System.out.println("Pruning blocks 2 to 900..."); + int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 900); + System.out.println("Number of blocks pruned (Expected 899): " + numBlocksPruned); + assertEquals(900-1, numBlocksPruned); + repository.getBlockRepository().setBlockPruneHeight(901); - // Prune the AT states for the archived blocks - System.out.println("Pruning AT states up to height 900..."); - repository.getATRepository().rebuildLatestAtStates(900); - repository.saveChanges(); - int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 900); - System.out.println("Number of AT states pruned (Expected 898): " + numATStatesPruned); - assertEquals(900-2, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state - repository.getATRepository().setAtPruneHeight(901); + // Prune the AT states for the archived blocks + System.out.println("Pruning AT states up to height 900..."); + repository.getATRepository().rebuildLatestAtStates(900); + repository.saveChanges(); + int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 900); + System.out.println("Number of AT states pruned (Expected 898): " + numATStatesPruned); + assertEquals(900-2, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state + repository.getATRepository().setAtPruneHeight(901); - // Now ensure the SQL repository is missing blocks 2 and 900... - System.out.println("Verifying that blocks 2 and 900 have been pruned..."); - assertNull(repository.getBlockRepository().fromHeight(2)); - assertNull(repository.getBlockRepository().fromHeight(900)); - System.out.println("Blocks 2 and 900 have been successfully pruned."); + // Now ensure the SQL repository is missing blocks 2 and 900... + System.out.println("Verifying that blocks 2 and 900 have been pruned..."); + assertNull(repository.getBlockRepository().fromHeight(2)); + assertNull(repository.getBlockRepository().fromHeight(900)); + System.out.println("Blocks 2 and 900 have been successfully pruned."); - // ... but it's not missing blocks 1 and 901 (we don't prune the genesis block) - System.out.println("Verifying that blocks 1 and 901 still exist..."); - assertNotNull(repository.getBlockRepository().fromHeight(1)); - assertNotNull(repository.getBlockRepository().fromHeight(901)); - System.out.println("Blocks 1 and 901 are present in the repository."); + // ... but it's not missing blocks 1 and 901 (we don't prune the genesis block) + System.out.println("Verifying that blocks 1 and 901 still exist..."); + assertNotNull(repository.getBlockRepository().fromHeight(1)); + assertNotNull(repository.getBlockRepository().fromHeight(901)); + System.out.println("Blocks 1 and 901 are present in the repository."); - // Validate the latest block height in the repository - int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); - System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); - assertEquals(1002, lastBlockHeight); + // Validate the latest block height in the repository + int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); + assertEquals(1002, lastBlockHeight); - System.out.println("testArchiveAndPrune completed successfully."); - } - } + System.out.println("testArchiveAndPrune completed successfully."); + } + } - @Test - public void testTrimArchivePruneAndOrphan() throws DataException, InterruptedException, TransformationException, IOException { - try (final Repository repository = RepositoryManager.getRepository()) { + @Test + public void testTrimArchivePruneAndOrphan() throws DataException, InterruptedException, TransformationException, IOException { + try (final Repository repository = RepositoryManager.getRepository()) { - System.out.println("Starting testTrimArchivePruneAndOrphan"); + System.out.println("Starting testTrimArchivePruneAndOrphan"); - // Deploy an AT so that we have AT state data - System.out.println("Deploying AT..."); - PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); - byte[] creationBytes = AtUtils.buildSimpleAT(); - long fundingAmount = 1_00000000L; - AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); - System.out.println("AT deployed successfully."); + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + byte[] creationBytes = AtUtils.buildSimpleAT(); + long fundingAmount = 1_00000000L; + AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + System.out.println("AT deployed successfully."); - // Mint some blocks so that we are able to archive them later - System.out.println("Minting 1000 blocks..."); - for (int i = 0; i < 1000; i++) { - BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); - // Log every 100 blocks - if ((i + 1) % 100 == 0) { - System.out.println("Minted block " + (i + 1)); - } - } - System.out.println("Finished minting blocks."); + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); + for (int i = 0; i < 1000; i++) { + BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } + } + System.out.println("Finished minting blocks."); - // Make sure that block 500 has full AT state data and data hash - System.out.println("Verifying block 500 AT state data..."); - List block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); - ATStateData atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); - assertNotNull(atStatesData.getStateHash()); - assertNotNull(atStatesData.getStateData()); - System.out.println("Block 500 AT state data verified."); + // Make sure that block 500 has full AT state data and data hash + System.out.println("Verifying block 500 AT state data..."); + List block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); + ATStateData atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); + assertNotNull(atStatesData.getStateHash()); + assertNotNull(atStatesData.getStateData()); + System.out.println("Block 500 AT state data verified."); - // Trim the first 500 blocks - System.out.println("Trimming first 500 blocks..."); - repository.getBlockRepository().trimOldOnlineAccountsSignatures(0, 500); - repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(501); - repository.getATRepository().rebuildLatestAtStates(500); - repository.getATRepository().trimAtStates(0, 500, 1000); - repository.getATRepository().setAtTrimHeight(501); - System.out.println("Trimming completed."); + // Trim the first 500 blocks + System.out.println("Trimming first 500 blocks..."); + repository.getBlockRepository().trimOldOnlineAccountsSignatures(0, 500); + repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(501); + repository.getATRepository().rebuildLatestAtStates(500); + repository.getATRepository().trimAtStates(0, 500, 1000); + repository.getATRepository().setAtTrimHeight(501); + System.out.println("Trimming completed."); - // Now block 499 should only have the AT state data hash - System.out.println("Checking block 499 AT state data..."); - List block499AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(499); - atStatesData = repository.getATRepository().getATStateAtHeight(block499AtStatesData.get(0).getATAddress(), 499); - assertNotNull(atStatesData.getStateHash()); - assertNull(atStatesData.getStateData()); - System.out.println("Block 499 AT state data contains only state hash as expected."); + // Now block 499 should only have the AT state data hash + System.out.println("Checking block 499 AT state data..."); + List block499AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(499); + atStatesData = repository.getATRepository().getATStateAtHeight(block499AtStatesData.get(0).getATAddress(), 499); + assertNotNull(atStatesData.getStateHash()); + assertNull(atStatesData.getStateData()); + System.out.println("Block 499 AT state data contains only state hash as expected."); - // ... but block 500 should have the full data (due to being retained as the "latest" AT state in the trimmed range - System.out.println("Verifying block 500 AT state data again..."); - block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); - atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); - assertNotNull(atStatesData.getStateHash()); - assertNotNull(atStatesData.getStateData()); - System.out.println("Block 500 AT state data contains full data."); + // ... but block 500 should have the full data (due to being retained as the "latest" AT state in the trimmed range + System.out.println("Verifying block 500 AT state data again..."); + block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); + atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); + assertNotNull(atStatesData.getStateHash()); + assertNotNull(atStatesData.getStateData()); + System.out.println("Block 500 AT state data contains full data."); - // ... and block 501 should also have the full data - System.out.println("Verifying block 501 AT state data..."); - List block501AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(501); - atStatesData = repository.getATRepository().getATStateAtHeight(block501AtStatesData.get(0).getATAddress(), 501); - assertNotNull(atStatesData.getStateHash()); - assertNotNull(atStatesData.getStateData()); - System.out.println("Block 501 AT state data contains full data."); + // ... and block 501 should also have the full data + System.out.println("Verifying block 501 AT state data..."); + List block501AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(501); + atStatesData = repository.getATRepository().getATStateAtHeight(block501AtStatesData.get(0).getATAddress(), 501); + assertNotNull(atStatesData.getStateHash()); + assertNotNull(atStatesData.getStateData()); + System.out.println("Block 501 AT state data contains full data."); - // Check the max archive height - this should be one less than the first untrimmed height - final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); - System.out.println("Maximum archive height determined (Expected 500): " + maximumArchiveHeight); - assertEquals(500, maximumArchiveHeight); + // Check the max archive height - this should be one less than the first untrimmed height + final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height determined (Expected 500): " + maximumArchiveHeight); + assertEquals(500, maximumArchiveHeight); - BlockData block3DataPreArchive = repository.getBlockRepository().fromHeight(3); + BlockData block3DataPreArchive = repository.getBlockRepository().fromHeight(3); - // Write blocks 2-500 to the archive - System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); - BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); - writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes - BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); - System.out.println("Finished writing blocks to archive. Result: " + result); - assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); + // Write blocks 2-500 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); + BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); + writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes + BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); + assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); - // Make sure that the archive contains the correct number of blocks - System.out.println("Number of blocks written to archive (Expected 499): " + writer.getWrittenCount()); - assertEquals(500 - 1, writer.getWrittenCount()); // -1 for the genesis block + // Make sure that the archive contains the correct number of blocks + System.out.println("Number of blocks written to archive (Expected 499): " + writer.getWrittenCount()); + assertEquals(500 - 1, writer.getWrittenCount()); // -1 for the genesis block - // Increment block archive height - repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); - repository.saveChanges(); - assertEquals(500 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); - System.out.println("Block archive height updated to: " + (500 - 1)); + // Increment block archive height + repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); + repository.saveChanges(); + assertEquals(500 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + System.out.println("Block archive height updated to: " + (500 - 1)); - // Ensure the file exists - File outputFile = writer.getOutputPath().toFile(); - assertTrue(outputFile.exists()); - System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + // Ensure the file exists + File outputFile = writer.getOutputPath().toFile(); + assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); - // Ensure the SQL repository contains blocks 2 and 500... - System.out.println("Verifying that blocks 2 and 500 exist in the repository..."); - assertNotNull(repository.getBlockRepository().fromHeight(2)); - assertNotNull(repository.getBlockRepository().fromHeight(500)); - System.out.println("Blocks 2 and 500 are present in the repository."); + // Ensure the SQL repository contains blocks 2 and 500... + System.out.println("Verifying that blocks 2 and 500 exist in the repository..."); + assertNotNull(repository.getBlockRepository().fromHeight(2)); + assertNotNull(repository.getBlockRepository().fromHeight(500)); + System.out.println("Blocks 2 and 500 are present in the repository."); - // Prune all the archived blocks - System.out.println("Pruning blocks 2 to 500..."); - int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 500); - System.out.println("Number of blocks pruned (Expected 499): " + numBlocksPruned); - assertEquals(500-1, numBlocksPruned); - repository.getBlockRepository().setBlockPruneHeight(501); + // Prune all the archived blocks + System.out.println("Pruning blocks 2 to 500..."); + int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 500); + System.out.println("Number of blocks pruned (Expected 499): " + numBlocksPruned); + assertEquals(500-1, numBlocksPruned); + repository.getBlockRepository().setBlockPruneHeight(501); - // Prune the AT states for the archived blocks - System.out.println("Pruning AT states up to height 500..."); - repository.getATRepository().rebuildLatestAtStates(500); - repository.saveChanges(); - int numATStatesPruned = repository.getATRepository().pruneAtStates(2, 500); - System.out.println("Number of AT states pruned (Expected 498): " + numATStatesPruned); - assertEquals(498, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state - repository.getATRepository().setAtPruneHeight(501); + // Prune the AT states for the archived blocks + System.out.println("Pruning AT states up to height 500..."); + repository.getATRepository().rebuildLatestAtStates(500); + repository.saveChanges(); + int numATStatesPruned = repository.getATRepository().pruneAtStates(2, 500); + System.out.println("Number of AT states pruned (Expected 498): " + numATStatesPruned); + assertEquals(498, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state + repository.getATRepository().setAtPruneHeight(501); - // Now ensure the SQL repository is missing blocks 2 and 500... - System.out.println("Verifying that blocks 2 and 500 have been pruned..."); - assertNull(repository.getBlockRepository().fromHeight(2)); - assertNull(repository.getBlockRepository().fromHeight(500)); - System.out.println("Blocks 2 and 500 have been successfully pruned."); + // Now ensure the SQL repository is missing blocks 2 and 500... + System.out.println("Verifying that blocks 2 and 500 have been pruned..."); + assertNull(repository.getBlockRepository().fromHeight(2)); + assertNull(repository.getBlockRepository().fromHeight(500)); + System.out.println("Blocks 2 and 500 have been successfully pruned."); - // ... but it's not missing blocks 1 and 501 (we don't prune the genesis block) - System.out.println("Verifying that blocks 1 and 501 still exist..."); - assertNotNull(repository.getBlockRepository().fromHeight(1)); - assertNotNull(repository.getBlockRepository().fromHeight(501)); - System.out.println("Blocks 1 and 501 are present in the repository."); + // ... but it's not missing blocks 1 and 501 (we don't prune the genesis block) + System.out.println("Verifying that blocks 1 and 501 still exist..."); + assertNotNull(repository.getBlockRepository().fromHeight(1)); + assertNotNull(repository.getBlockRepository().fromHeight(501)); + System.out.println("Blocks 1 and 501 are present in the repository."); - // Validate the latest block height in the repository - int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); - System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); - assertEquals(1002, lastBlockHeight); + // Validate the latest block height in the repository + int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); + assertEquals(1002, lastBlockHeight); - // Now orphan some unarchived blocks. - System.out.println("Orphaning 500 blocks..."); - BlockUtils.orphanBlocks(repository, 500); - int currentLastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); - System.out.println("New last block height after orphaning (Expected 502): " + currentLastBlockHeight); - assertEquals(502, currentLastBlockHeight); + // Now orphan some unarchived blocks. + System.out.println("Orphaning 500 blocks..."); + BlockUtils.orphanBlocks(repository, 500); + int currentLastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("New last block height after orphaning (Expected 502): " + currentLastBlockHeight); + assertEquals(502, currentLastBlockHeight); - // We're close to the lower limit of the SQL database now, so - // we need to import some blocks from the archive - System.out.println("Importing blocks 401 to 500 from the archive..."); - BlockArchiveUtils.importFromArchive(401, 500, repository); + // We're close to the lower limit of the SQL database now, so + // we need to import some blocks from the archive + System.out.println("Importing blocks 401 to 500 from the archive..."); + BlockArchiveUtils.importFromArchive(401, 500, repository); - // Ensure the SQL repository now contains block 401 but not 400... - System.out.println("Verifying that block 401 exists and block 400 does not..."); - assertNotNull(repository.getBlockRepository().fromHeight(401)); - assertNull(repository.getBlockRepository().fromHeight(400)); - System.out.println("Block 401 exists, block 400 does not."); + // Ensure the SQL repository now contains block 401 but not 400... + System.out.println("Verifying that block 401 exists and block 400 does not..."); + assertNotNull(repository.getBlockRepository().fromHeight(401)); + assertNull(repository.getBlockRepository().fromHeight(400)); + System.out.println("Block 401 exists, block 400 does not."); - // Import the remaining 399 blocks - System.out.println("Importing blocks 2 to 400 from the archive..."); - BlockArchiveUtils.importFromArchive(2, 400, repository); + // Import the remaining 399 blocks + System.out.println("Importing blocks 2 to 400 from the archive..."); + BlockArchiveUtils.importFromArchive(2, 400, repository); - // Verify that block 3 matches the original - System.out.println("Verifying that block 3 matches the original data..."); - BlockData block3DataPostArchive = repository.getBlockRepository().fromHeight(3); - assertArrayEquals(block3DataPreArchive.getSignature(), block3DataPostArchive.getSignature()); - assertEquals(block3DataPreArchive.getHeight(), block3DataPostArchive.getHeight()); - System.out.println("Block 3 data matches the original."); + // Verify that block 3 matches the original + System.out.println("Verifying that block 3 matches the original data..."); + BlockData block3DataPostArchive = repository.getBlockRepository().fromHeight(3); + assertArrayEquals(block3DataPreArchive.getSignature(), block3DataPostArchive.getSignature()); + assertEquals(block3DataPreArchive.getHeight(), block3DataPostArchive.getHeight()); + System.out.println("Block 3 data matches the original."); - // Orphan 2 more block, which should be the last one that is possible to be orphaned + // Orphan 2 more block, which should be the last one that is possible to be orphaned // TODO: figure out why this is 1 block more than in the equivalent block archive V1 test - System.out.println("Orphaning 2 more blocks..."); - BlockUtils.orphanBlocks(repository, 2); - System.out.println("Orphaned 2 blocks successfully."); + System.out.println("Orphaning 2 more blocks..."); + BlockUtils.orphanBlocks(repository, 2); + System.out.println("Orphaned 2 blocks successfully."); - // Orphan another block, which should fail - System.out.println("Attempting to orphan another block, which should fail..."); - Exception exception = null; - try { - BlockUtils.orphanBlocks(repository, 1); - } catch (DataException e) { - exception = e; - System.out.println("Caught expected DataException: " + e.getMessage()); - } + // Orphan another block, which should fail + System.out.println("Attempting to orphan another block, which should fail..."); + Exception exception = null; + try { + BlockUtils.orphanBlocks(repository, 1); + } catch (DataException e) { + exception = e; + System.out.println("Caught expected DataException: " + e.getMessage()); + } - // Ensure that a DataException is thrown because there is no more AT states data available - assertNotNull(exception); - assertEquals(DataException.class, exception.getClass()); - System.out.println("DataException confirmed due to lack of AT states data."); + // Ensure that a DataException is thrown because there is no more AT states data available + assertNotNull(exception); + assertEquals(DataException.class, exception.getClass()); + System.out.println("DataException confirmed due to lack of AT states data."); - // FUTURE: we may be able to retain unique AT states when trimming, to avoid this exception - // and allow orphaning back through blocks with trimmed AT states. + // FUTURE: we may be able to retain unique AT states when trimming, to avoid this exception + // and allow orphaning back through blocks with trimmed AT states. - System.out.println("testTrimArchivePruneAndOrphan completed successfully."); - } - } + System.out.println("testTrimArchivePruneAndOrphan completed successfully."); + } + } - /** - * Many nodes are missing an ATStatesHeightIndex due to an earlier bug - * In these cases we disable archiving and pruning as this index is a - * very essential component in these processes. - */ - @Test - public void testMissingAtStatesHeightIndex() throws DataException, SQLException { - try (final HSQLDBRepository repository = (HSQLDBRepository) RepositoryManager.getRepository()) { + /** + * Many nodes are missing an ATStatesHeightIndex due to an earlier bug + * In these cases we disable archiving and pruning as this index is a + * very essential component in these processes. + */ + @Test + public void testMissingAtStatesHeightIndex() throws DataException, SQLException { + try (final HSQLDBRepository repository = (HSQLDBRepository) RepositoryManager.getRepository()) { - System.out.println("Starting testMissingAtStatesHeightIndex"); + System.out.println("Starting testMissingAtStatesHeightIndex"); - // Firstly check that we're able to prune or archive when the index exists - System.out.println("Checking existence of ATStatesHeightIndex..."); - assertTrue(repository.getATRepository().hasAtStatesHeightIndex()); - assertTrue(RepositoryManager.canArchiveOrPrune()); - System.out.println("ATStatesHeightIndex exists. Archiving and pruning are possible."); + // Firstly check that we're able to prune or archive when the index exists + System.out.println("Checking existence of ATStatesHeightIndex..."); + assertTrue(repository.getATRepository().hasAtStatesHeightIndex()); + assertTrue(RepositoryManager.canArchiveOrPrune()); + System.out.println("ATStatesHeightIndex exists. Archiving and pruning are possible."); - // Delete the index - System.out.println("Dropping ATStatesHeightIndex..."); - repository.prepareStatement("DROP INDEX ATSTATESHEIGHTINDEX").execute(); - System.out.println("ATStatesHeightIndex dropped."); + // Delete the index + System.out.println("Dropping ATStatesHeightIndex..."); + repository.prepareStatement("DROP INDEX ATSTATESHEIGHTINDEX").execute(); + System.out.println("ATStatesHeightIndex dropped."); - // Ensure check that we're unable to prune or archive when the index doesn't exist - System.out.println("Verifying that ATStatesHeightIndex no longer exists..."); - assertFalse(repository.getATRepository().hasAtStatesHeightIndex()); - assertFalse(RepositoryManager.canArchiveOrPrune()); - System.out.println("ATStatesHeightIndex does not exist. Archiving and pruning are disabled."); + // Ensure check that we're unable to prune or archive when the index doesn't exist + System.out.println("Verifying that ATStatesHeightIndex no longer exists..."); + assertFalse(repository.getATRepository().hasAtStatesHeightIndex()); + assertFalse(RepositoryManager.canArchiveOrPrune()); + System.out.println("ATStatesHeightIndex does not exist. Archiving and pruning are disabled."); - System.out.println("testMissingAtStatesHeightIndex completed successfully."); - } - } + System.out.println("testMissingAtStatesHeightIndex completed successfully."); + } + } - private void deleteArchiveDirectory() { - // Delete archive directory if exists - Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toAbsolutePath(); - try { - FileUtils.deleteDirectory(archivePath.toFile()); - System.out.println("Deleted archive directory at: " + archivePath); - } catch (IOException e) { + private void deleteArchiveDirectory() { + // Delete archive directory if exists + Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toAbsolutePath(); + try { + FileUtils.deleteDirectory(archivePath.toFile()); + System.out.println("Deleted archive directory at: " + archivePath); + } catch (IOException e) { - System.out.println("Failed to delete archive directory: " + e.getMessage()); - } - } + System.out.println("Failed to delete archive directory: " + e.getMessage()); + } + } } From 652c9026072339b0908ecbdd181ff43dbbe70586 Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Sun, 17 Nov 2024 16:45:39 -0500 Subject: [PATCH 04/16] Add missing feature triggers to unit tests --- src/test/resources/test-chain-v2-block-timestamps.json | 8 +++++--- src/test/resources/test-chain-v2-disable-reference.json | 8 +++++--- src/test/resources/test-chain-v2-founder-rewards.json | 8 +++++--- src/test/resources/test-chain-v2-leftover-reward.json | 8 +++++--- src/test/resources/test-chain-v2-minting.json | 8 +++++--- src/test/resources/test-chain-v2-penalty-fix.json | 6 ++++-- .../resources/test-chain-v2-qora-holder-extremes.json | 8 +++++--- .../resources/test-chain-v2-qora-holder-reduction.json | 8 +++++--- src/test/resources/test-chain-v2-qora-holder.json | 8 +++++--- src/test/resources/test-chain-v2-reward-levels.json | 8 +++++--- src/test/resources/test-chain-v2-reward-scaling.json | 8 +++++--- src/test/resources/test-chain-v2-reward-shares.json | 8 +++++--- .../resources/test-chain-v2-self-sponsorship-algo-v1.json | 8 +++++--- .../resources/test-chain-v2-self-sponsorship-algo-v2.json | 8 +++++--- .../resources/test-chain-v2-self-sponsorship-algo-v3.json | 8 +++++--- src/test/resources/test-chain-v2.json | 8 +++++--- 16 files changed, 79 insertions(+), 47 deletions(-) diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json index b2f0119d..4e49e86d 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -81,7 +81,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 9999999999999, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -95,8 +95,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json index 86ed264f..9ad59d79 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -84,7 +84,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 0, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -98,8 +98,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index d1b9c3c4..e4182d7d 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -99,8 +99,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index 106ac7dd..04005b2b 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -99,8 +99,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index 159b2dd7..ddb29ca5 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 0, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -99,8 +99,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-penalty-fix.json b/src/test/resources/test-chain-v2-penalty-fix.json index 2266b032..cac92c16 100644 --- a/src/test/resources/test-chain-v2-penalty-fix.json +++ b/src/test/resources/test-chain-v2-penalty-fix.json @@ -83,7 +83,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -97,8 +97,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999, "penaltyFixHeight": 5 }, "genesisInfo": { diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json index 6043f15c..566d8515 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -99,8 +99,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder-reduction.json b/src/test/resources/test-chain-v2-qora-holder-reduction.json index 7727a283..c7ed2270 100644 --- a/src/test/resources/test-chain-v2-qora-holder-reduction.json +++ b/src/test/resources/test-chain-v2-qora-holder-reduction.json @@ -86,7 +86,7 @@ "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, "aggregateSignatureTimestamp": 0, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -100,8 +100,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index 6b9f9d54..1c4f0d93 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -99,8 +99,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index 6f0993d0..30d952e1 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -99,8 +99,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index d1d4519a..612f02a5 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -99,8 +99,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json index 69edc540..2f332233 100644 --- a/src/test/resources/test-chain-v2-reward-shares.json +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -99,8 +99,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json b/src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json index ad2ad4b1..3ea8bc70 100644 --- a/src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 20, "selfSponsorshipAlgoV2Height": 999999999, @@ -99,8 +99,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json b/src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json index b2812c05..ae424704 100644 --- a/src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 30, @@ -99,8 +99,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json b/src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json index d65aa48e..2a24473b 100644 --- a/src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -99,8 +99,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 086c126e..c829975b 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -86,7 +86,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -100,8 +100,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, From 7803d6c8f5db2530e9b51489b5c3b5402f9f2817 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Mon, 25 Nov 2024 09:36:11 +0200 Subject: [PATCH 05/16] adjust timeouts for qortalrequests --- src/main/resources/q-apps/q-apps.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/resources/q-apps/q-apps.js b/src/main/resources/q-apps/q-apps.js index e8a42537..25656370 100644 --- a/src/main/resources/q-apps/q-apps.js +++ b/src/main/resources/q-apps/q-apps.js @@ -614,6 +614,7 @@ function getDefaultTimeout(action) { switch (action) { case "GET_USER_ACCOUNT": case "SAVE_FILE": + case "SIGN_TRANSACTION": case "DECRYPT_DATA": // User may take a long time to accept/deny the popup return 60 * 60 * 1000; @@ -635,6 +636,11 @@ function getDefaultTimeout(action) { // Chat messages rely on PoW computations, so allow extra time return 60 * 1000; + case "CREATE_TRADE_BUY_ORDER": + case "CREATE_TRADE_SELL_ORDER": + case "CANCEL_TRADE_SELL_ORDER": + case "VOTE_ON_POLL": + case "CREATE_POLL": case "JOIN_GROUP": case "DEPLOY_AT": case "SEND_COIN": @@ -649,7 +655,7 @@ function getDefaultTimeout(action) { break; } } - return 10 * 1000; + return 30 * 1000; } /** From 61dec0e4b7e463febc78974d107231c56b48c5b7 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Sun, 1 Dec 2024 12:38:38 +0200 Subject: [PATCH 06/16] add haschatreference query to activechats endpoint --- .../org/qortal/api/resource/ChatResource.java | 12 ++-- .../api/websocket/ActiveChatsWebSocket.java | 20 +++++- .../org/qortal/repository/ChatRepository.java | 2 +- .../hsqldb/HSQLDBChatRepository.java | 62 +++++++++++++------ .../java/org/qortal/test/RepositoryTests.java | 11 +++- 5 files changed, 81 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/ChatResource.java b/src/main/java/org/qortal/api/resource/ChatResource.java index 66a2bd46..df2ca399 100644 --- a/src/main/java/org/qortal/api/resource/ChatResource.java +++ b/src/main/java/org/qortal/api/resource/ChatResource.java @@ -234,17 +234,21 @@ public class ChatResource { } ) @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE}) - public ActiveChats getActiveChats(@PathParam("address") String address, @QueryParam("encoding") Encoding encoding) { + public ActiveChats getActiveChats( + @PathParam("address") String address, + @QueryParam("encoding") Encoding encoding, + @QueryParam("haschatreference") Boolean hasChatReference + ) { if (address == null || !Crypto.isValidAddress(address)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); - + try (final Repository repository = RepositoryManager.getRepository()) { - return repository.getChatRepository().getActiveChats(address, encoding); + return repository.getChatRepository().getActiveChats(address, encoding, hasChatReference); } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); } } - + @POST @Operation( summary = "Build raw, unsigned, CHAT transaction", diff --git a/src/main/java/org/qortal/api/websocket/ActiveChatsWebSocket.java b/src/main/java/org/qortal/api/websocket/ActiveChatsWebSocket.java index b92fb19f..ca3ef2b3 100644 --- a/src/main/java/org/qortal/api/websocket/ActiveChatsWebSocket.java +++ b/src/main/java/org/qortal/api/websocket/ActiveChatsWebSocket.java @@ -77,7 +77,9 @@ public class ActiveChatsWebSocket extends ApiWebSocket { } try (final Repository repository = RepositoryManager.getRepository()) { - ActiveChats activeChats = repository.getChatRepository().getActiveChats(ourAddress, getTargetEncoding(session)); + Boolean hasChatReference = getHasChatReference(session); + + ActiveChats activeChats = repository.getChatRepository().getActiveChats(ourAddress, getTargetEncoding(session), hasChatReference); StringWriter stringWriter = new StringWriter(); @@ -103,4 +105,20 @@ public class ActiveChatsWebSocket extends ApiWebSocket { return Encoding.valueOf(encoding); } + private Boolean getHasChatReference(Session session) { + Map> queryParams = session.getUpgradeRequest().getParameterMap(); + List hasChatReferenceList = queryParams.get("haschatreference"); + + // Return null if not specified + if (hasChatReferenceList != null && hasChatReferenceList.size() == 1) { + String value = hasChatReferenceList.get(0).toLowerCase(); + if (value.equals("true")) { + return true; + } else if (value.equals("false")) { + return false; + } + } + return null; // Ignored if not present + } + } diff --git a/src/main/java/org/qortal/repository/ChatRepository.java b/src/main/java/org/qortal/repository/ChatRepository.java index d046fe6b..bd636fe3 100644 --- a/src/main/java/org/qortal/repository/ChatRepository.java +++ b/src/main/java/org/qortal/repository/ChatRepository.java @@ -22,6 +22,6 @@ public interface ChatRepository { public ChatMessage toChatMessage(ChatTransactionData chatTransactionData, Encoding encoding) throws DataException; - public ActiveChats getActiveChats(String address, Encoding encoding) throws DataException; + public ActiveChats getActiveChats(String address, Encoding encoding, Boolean hasChatReference) throws DataException; } diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java index 571a587d..80865739 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java @@ -23,7 +23,7 @@ public class HSQLDBChatRepository implements ChatRepository { public HSQLDBChatRepository(HSQLDBRepository repository) { this.repository = repository; } - + @Override public List getMessagesMatchingCriteria(Long before, Long after, Integer txGroupId, byte[] referenceBytes, byte[] chatReferenceBytes, Boolean hasChatReference, List involving, String senderAddress, @@ -176,14 +176,14 @@ public class HSQLDBChatRepository implements ChatRepository { } @Override - public ActiveChats getActiveChats(String address, Encoding encoding) throws DataException { - List groupChats = getActiveGroupChats(address, encoding); - List directChats = getActiveDirectChats(address); + public ActiveChats getActiveChats(String address, Encoding encoding, Boolean hasChatReference) throws DataException { + List groupChats = getActiveGroupChats(address, encoding, hasChatReference); + List directChats = getActiveDirectChats(address, hasChatReference); return new ActiveChats(groupChats, directChats); } - - private List getActiveGroupChats(String address, Encoding encoding) throws DataException { + + private List getActiveGroupChats(String address, Encoding encoding, Boolean hasChatReference) throws DataException { // Find groups where address is a member and potential latest message details String groupsSql = "SELECT group_id, group_name, latest_timestamp, sender, sender_name, signature, data " + "FROM GroupMembers " @@ -194,11 +194,19 @@ public class HSQLDBChatRepository implements ChatRepository { + "JOIN Transactions USING (signature) " + "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender " // NOTE: We need to qualify "Groups.group_id" here to avoid "General error" bug in HSQLDB v2.5.0 - + "WHERE tx_group_id = Groups.group_id AND type = " + TransactionType.CHAT.value + " " - + "ORDER BY created_when DESC " + + "WHERE tx_group_id = Groups.group_id AND type = " + TransactionType.CHAT.value + " "; + + if (hasChatReference != null) { + if (hasChatReference) { + groupsSql += "AND chat_reference IS NOT NULL "; + } else { + groupsSql += "AND chat_reference IS NULL "; + } + } + groupsSql += "ORDER BY created_when DESC " + "LIMIT 1" - + ") AS LatestMessages ON TRUE " - + "WHERE address = ?"; + + ") AS LatestMessages ON TRUE " + + "WHERE address = ?"; List groupChats = new ArrayList<>(); try (ResultSet resultSet = this.repository.checkedExecute(groupsSql, address)) { @@ -230,8 +238,16 @@ public class HSQLDBChatRepository implements ChatRepository { + "JOIN Transactions USING (signature) " + "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender " + "WHERE tx_group_id = 0 " - + "AND recipient IS NULL " - + "ORDER BY created_when DESC " + + "AND recipient IS NULL "; + + if (hasChatReference != null) { + if (hasChatReference) { + grouplessSql += "AND chat_reference IS NOT NULL "; + } else { + grouplessSql += "AND chat_reference IS NULL "; + } + } + grouplessSql += "ORDER BY created_when DESC " + "LIMIT 1"; try (ResultSet resultSet = this.repository.checkedExecute(grouplessSql)) { @@ -259,7 +275,7 @@ public class HSQLDBChatRepository implements ChatRepository { return groupChats; } - private List getActiveDirectChats(String address) throws DataException { + private List getActiveDirectChats(String address, Boolean hasChatReference) throws DataException { // Find chat messages involving address String directSql = "SELECT other_address, name, latest_timestamp, sender, sender_name " + "FROM (" @@ -275,11 +291,21 @@ public class HSQLDBChatRepository implements ChatRepository { + "NATURAL JOIN Transactions " + "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender " + "WHERE (sender = other_address AND recipient = ?) " - + "OR (sender = ? AND recipient = other_address) " - + "ORDER BY created_when DESC " - + "LIMIT 1" - + ") AS LatestMessages " - + "LEFT OUTER JOIN Names ON owner = other_address"; + + "OR (sender = ? AND recipient = other_address) "; + + // Apply hasChatReference filter + if (hasChatReference != null) { + if (hasChatReference) { + directSql += "AND chat_reference IS NOT NULL "; + } else { + directSql += "AND chat_reference IS NULL "; + } + } + + directSql += "ORDER BY created_when DESC " + + "LIMIT 1" + + ") AS LatestMessages " + + "LEFT OUTER JOIN Names ON owner = other_address"; Object[] bindParams = new Object[] { address, address, address, address }; diff --git a/src/test/java/org/qortal/test/RepositoryTests.java b/src/test/java/org/qortal/test/RepositoryTests.java index 0d07be4b..1b0a0e52 100644 --- a/src/test/java/org/qortal/test/RepositoryTests.java +++ b/src/test/java/org/qortal/test/RepositoryTests.java @@ -411,13 +411,20 @@ public class RepositoryTests extends Common { } } - /** Specifically test LATERAL() usage in Chat repository */ + /** Specifically test LATERAL() usage in Chat repository with hasChatReference */ @Test public void testChatLateral() { try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) { String address = Crypto.toAddress(new byte[32]); - hsqldb.getChatRepository().getActiveChats(address, ChatMessage.Encoding.BASE58); + // Test without hasChatReference + hsqldb.getChatRepository().getActiveChats(address, ChatMessage.Encoding.BASE58, null); + + // Test with hasChatReference = true + hsqldb.getChatRepository().getActiveChats(address, ChatMessage.Encoding.BASE58, true); + + // Test with hasChatReference = false + hsqldb.getChatRepository().getActiveChats(address, ChatMessage.Encoding.BASE58, false); } catch (DataException e) { fail("HSQLDB bug #1580"); } From adbba0f94767cda6251668c5206015dfccb44941 Mon Sep 17 00:00:00 2001 From: AlphaX-Qortal Date: Mon, 2 Dec 2024 14:22:05 +0100 Subject: [PATCH 07/16] Various changes - Added real address to API results - Added group member check to validations - Network changes --- src/main/java/org/qortal/account/Account.java | 20 +++- .../qortal/api/model/ApiOnlineAccount.java | 33 +++++++ .../qortal/api/model/BlockMintingInfo.java | 2 +- .../qortal/api/resource/BlocksResource.java | 3 +- src/main/java/org/qortal/block/Block.java | 51 +++++++--- .../java/org/qortal/data/block/BlockData.java | 29 +++++- .../java/org/qortal/network/Handshake.java | 26 ++--- src/main/java/org/qortal/network/Network.java | 98 +++++++++---------- 8 files changed, 180 insertions(+), 82 deletions(-) diff --git a/src/main/java/org/qortal/account/Account.java b/src/main/java/org/qortal/account/Account.java index 537f0788..99fa5217 100644 --- a/src/main/java/org/qortal/account/Account.java +++ b/src/main/java/org/qortal/account/Account.java @@ -349,10 +349,28 @@ public class Account { } /** - * Returns 'effective' minting level, or zero if reward-share does not exist. + * Returns reward-share minting address, or unknown if reward-share does not exist. * * @param repository * @param rewardSharePublicKey + * @return address or unknown + * @throws DataException + */ + public static String getRewardShareMintingAddress(Repository repository, byte[] rewardSharePublicKey) throws DataException { + // Find actual minter address + RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(rewardSharePublicKey); + + if (rewardShareData == null) + return "Unknown"; + + return rewardShareData.getMinter(); + } + + /** + * Returns 'effective' minting level, or zero if reward-share does not exist. + * + * @param repository + * @param rewardSharePublicKey * @return 0+ * @throws DataException */ diff --git a/src/main/java/org/qortal/api/model/ApiOnlineAccount.java b/src/main/java/org/qortal/api/model/ApiOnlineAccount.java index 08b697aa..e26eb816 100644 --- a/src/main/java/org/qortal/api/model/ApiOnlineAccount.java +++ b/src/main/java/org/qortal/api/model/ApiOnlineAccount.java @@ -1,7 +1,13 @@ package org.qortal.api.model; +import org.qortal.account.Account; +import org.qortal.repository.DataException; +import org.qortal.repository.RepositoryManager; +import org.qortal.repository.Repository; + import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; // All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) @@ -47,4 +53,31 @@ public class ApiOnlineAccount { return this.recipientAddress; } + public int getMinterLevelFromPublicKey() { + try (final Repository repository = RepositoryManager.getRepository()) { + return Account.getRewardShareEffectiveMintingLevel(repository, this.rewardSharePublicKey); + } catch (DataException e) { + return 0; + } + } + + public boolean getIsMember() { + try (final Repository repository = RepositoryManager.getRepository()) { + return repository.getGroupRepository().memberExists(694, getMinterAddress()); + } catch (DataException e) { + return false; + } + } + + // JAXB special + + @XmlElement(name = "minterLevel") + protected int getMinterLevel() { + return getMinterLevelFromPublicKey(); + } + + @XmlElement(name = "isMinterMember") + protected boolean getMinterMember() { + return getIsMember(); + } } diff --git a/src/main/java/org/qortal/api/model/BlockMintingInfo.java b/src/main/java/org/qortal/api/model/BlockMintingInfo.java index f84e179e..02765a89 100644 --- a/src/main/java/org/qortal/api/model/BlockMintingInfo.java +++ b/src/main/java/org/qortal/api/model/BlockMintingInfo.java @@ -9,6 +9,7 @@ import java.math.BigInteger; public class BlockMintingInfo { public byte[] minterPublicKey; + public String minterAddress; public int minterLevel; public int onlineAccountsCount; public BigDecimal maxDistance; @@ -19,5 +20,4 @@ public class BlockMintingInfo { public BlockMintingInfo() { } - } diff --git a/src/main/java/org/qortal/api/resource/BlocksResource.java b/src/main/java/org/qortal/api/resource/BlocksResource.java index 01d8d2ab..ff0bb979 100644 --- a/src/main/java/org/qortal/api/resource/BlocksResource.java +++ b/src/main/java/org/qortal/api/resource/BlocksResource.java @@ -542,6 +542,7 @@ public class BlocksResource { } } + String minterAddress = Account.getRewardShareMintingAddress(repository, blockData.getMinterPublicKey()); int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey()); if (minterLevel == 0) // This may be unavailable when requesting a trimmed block @@ -554,6 +555,7 @@ public class BlocksResource { BlockMintingInfo blockMintingInfo = new BlockMintingInfo(); blockMintingInfo.minterPublicKey = blockData.getMinterPublicKey(); + blockMintingInfo.minterAddress = minterAddress; blockMintingInfo.minterLevel = minterLevel; blockMintingInfo.onlineAccountsCount = blockData.getOnlineAccountsCount(); blockMintingInfo.maxDistance = new BigDecimal(block.MAX_DISTANCE); @@ -887,5 +889,4 @@ public class BlocksResource { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); } } - } diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 918a20ae..c9353d70 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -145,7 +145,7 @@ public class Block { private final Account recipientAccount; private final AccountData recipientAccountData; - + final BlockChain blockChain = BlockChain.getInstance(); ExpandedAccount(Repository repository, RewardShareData rewardShareData) throws DataException { @@ -414,6 +414,21 @@ public class Block { }); } + // After feature trigger, remove any online accounts that are not minter group member + if (height >= BlockChain.getInstance().getGroupMemberCheckHeight()) { + onlineAccounts.removeIf(a -> { + try { + int groupId = BlockChain.getInstance().getMintingGroupId(); + String address = Account.getRewardShareMintingAddress(repository, a.getPublicKey()); + boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address); + return !isMinterGroupMember; + } catch (DataException e) { + // Something went wrong, so remove the account + return true; + } + }); + } + if (onlineAccounts.isEmpty()) { LOGGER.debug("No online accounts - not even our own?"); return null; @@ -721,19 +736,19 @@ public class Block { List expandedAccounts = new ArrayList<>(); for (RewardShareData rewardShare : this.cachedOnlineRewardShares) { - if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) { + int groupId = BlockChain.getInstance().getMintingGroupId(); + String address = rewardShare.getMinter(); + boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address); + + if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) + expandedAccounts.add(new ExpandedAccount(repository, rewardShare)); + + if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight() && isMinterGroupMember) expandedAccounts.add(new ExpandedAccount(repository, rewardShare)); - } - if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight()) { - boolean isMinterGroupMember = repository.getGroupRepository().memberExists(BlockChain.getInstance().getMintingGroupId(), rewardShare.getMinter()); - if (isMinterGroupMember) { - expandedAccounts.add(new ExpandedAccount(repository, rewardShare)); - } - } } - this.cachedExpandedAccounts = expandedAccounts; + LOGGER.trace(() -> String.format("Online reward-shares after expanded accounts %s", this.cachedOnlineRewardShares)); return this.cachedExpandedAccounts; } @@ -1143,8 +1158,17 @@ public class Block { if (this.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) { List expandedAccounts = this.getExpandedAccounts(); for (ExpandedAccount account : expandedAccounts) { + int groupId = BlockChain.getInstance().getMintingGroupId(); + String address = account.getMintingAccount().getAddress(); + boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address); + if (account.getMintingAccount().getEffectiveMintingLevel() == 0) return ValidationResult.ONLINE_ACCOUNTS_INVALID; + + if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight()) { + if (!isMinterGroupMember) + return ValidationResult.ONLINE_ACCOUNTS_INVALID; + } } } @@ -1273,6 +1297,7 @@ public class Block { // Online Accounts ValidationResult onlineAccountsResult = this.areOnlineAccountsValid(); + LOGGER.trace("Accounts valid = {}", onlineAccountsResult); if (onlineAccountsResult != ValidationResult.OK) return onlineAccountsResult; @@ -1361,7 +1386,7 @@ public class Block { // Check transaction can even be processed validationResult = transaction.isProcessable(); if (validationResult != Transaction.ValidationResult.OK) { - LOGGER.info(String.format("Error during transaction validation, tx %s: %s", Base58.encode(transactionData.getSignature()), validationResult.name())); + LOGGER.debug(String.format("Error during transaction validation, tx %s: %s", Base58.encode(transactionData.getSignature()), validationResult.name())); return ValidationResult.TRANSACTION_INVALID; } @@ -1562,6 +1587,7 @@ public class Block { this.blockData.setHeight(blockchainHeight + 1); LOGGER.trace(() -> String.format("Processing block %d", this.blockData.getHeight())); + LOGGER.trace(() -> String.format("Online Reward Shares in process %s", this.cachedOnlineRewardShares)); if (this.blockData.getHeight() > 1) { @@ -2280,7 +2306,6 @@ public class Block { // Select the correct set of share bins based on block height List accountLevelShareBinsForBlock = (this.blockData.getHeight() >= BlockChain.getInstance().getSharesByLevelV2Height()) ? BlockChain.getInstance().getAccountLevelShareBinsV2() : BlockChain.getInstance().getAccountLevelShareBinsV1(); - // Determine reward candidates based on account level // This needs a deep copy, so the shares can be modified when tiers aren't activated yet List accountLevelShareBins = new ArrayList<>(); @@ -2570,9 +2595,11 @@ public class Block { return; int minterLevel = Account.getRewardShareEffectiveMintingLevel(this.repository, this.getMinter().getPublicKey()); + String minterAddress = Account.getRewardShareMintingAddress(this.repository, this.getMinter().getPublicKey()); LOGGER.debug(String.format("======= BLOCK %d (%.8s) =======", this.getBlockData().getHeight(), Base58.encode(this.getSignature()))); LOGGER.debug(String.format("Timestamp: %d", this.getBlockData().getTimestamp())); + LOGGER.debug(String.format("Minter address: %s", minterAddress)); LOGGER.debug(String.format("Minter level: %d", minterLevel)); LOGGER.debug(String.format("Online accounts: %d", this.getBlockData().getOnlineAccountsCount())); LOGGER.debug(String.format("AT count: %d", this.getBlockData().getATCount())); diff --git a/src/main/java/org/qortal/data/block/BlockData.java b/src/main/java/org/qortal/data/block/BlockData.java index 34df0f9a..7e2a1872 100644 --- a/src/main/java/org/qortal/data/block/BlockData.java +++ b/src/main/java/org/qortal/data/block/BlockData.java @@ -1,8 +1,11 @@ package org.qortal.data.block; import com.google.common.primitives.Bytes; +import org.qortal.account.Account; import org.qortal.block.BlockChain; -import org.qortal.crypto.Crypto; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; import org.qortal.settings.Settings; import org.qortal.utils.NTP; @@ -224,7 +227,7 @@ public class BlockData implements Serializable { } return 0; } - + public boolean isTrimmed() { long onlineAccountSignaturesTrimmedTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime(); long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime(); @@ -232,11 +235,31 @@ public class BlockData implements Serializable { return blockTimestamp < onlineAccountSignaturesTrimmedTimestamp && blockTimestamp < currentTrimmableTimestamp; } + public String getMinterAddressFromPublicKey() { + try (final Repository repository = RepositoryManager.getRepository()) { + return Account.getRewardShareMintingAddress(repository, this.minterPublicKey); + } catch (DataException e) { + return "Unknown"; + } + } + + public int getMinterLevelFromPublicKey() { + try (final Repository repository = RepositoryManager.getRepository()) { + return Account.getRewardShareEffectiveMintingLevel(repository, this.minterPublicKey); + } catch (DataException e) { + return 0; + } + } + // JAXB special @XmlElement(name = "minterAddress") protected String getMinterAddress() { - return Crypto.toAddress(this.minterPublicKey); + return getMinterAddressFromPublicKey(); } + @XmlElement(name = "minterLevel") + protected int getMinterLevel() { + return getMinterLevelFromPublicKey(); + } } diff --git a/src/main/java/org/qortal/network/Handshake.java b/src/main/java/org/qortal/network/Handshake.java index 081e79e6..07f14702 100644 --- a/src/main/java/org/qortal/network/Handshake.java +++ b/src/main/java/org/qortal/network/Handshake.java @@ -48,7 +48,7 @@ public enum Handshake { String versionString = helloMessage.getVersionString(); - Matcher matcher = peer.VERSION_PATTERN.matcher(versionString); + Matcher matcher = Peer.VERSION_PATTERN.matcher(versionString); if (!matcher.lookingAt()) { LOGGER.debug(() -> String.format("Peer %s sent invalid HELLO version string '%s'", peer, versionString)); return null; @@ -71,7 +71,7 @@ public enum Handshake { // Ensure the peer is running at least the version specified in MIN_PEER_VERSION if (!peer.isAtLeastVersion(MIN_PEER_VERSION)) { - LOGGER.debug(String.format("Ignoring peer %s because it is on an old version (%s)", peer, versionString)); + LOGGER.debug("Ignoring peer {} because it is on an old version ({})", peer, versionString); return null; } @@ -79,7 +79,7 @@ public enum Handshake { // Ensure the peer is running at least the minimum version allowed for connections final String minPeerVersion = Settings.getInstance().getMinPeerVersion(); if (!peer.isAtLeastVersion(minPeerVersion)) { - LOGGER.debug(String.format("Ignoring peer %s because it is on an old version (%s)", peer, versionString)); + LOGGER.debug("Ignoring peer {} because it is on an old version ({})", peer, versionString); return null; } } @@ -106,7 +106,7 @@ public enum Handshake { byte[] peersPublicKey = challengeMessage.getPublicKey(); byte[] peersChallenge = challengeMessage.getChallenge(); - // If public key matches our public key then we've connected to self + // If public key matches our public key, then we've connected to self byte[] ourPublicKey = Network.getInstance().getOurPublicKey(); if (Arrays.equals(ourPublicKey, peersPublicKey)) { // If outgoing connection then record destination as self so we don't try again @@ -121,11 +121,11 @@ public enum Handshake { peer.disconnect("failed to send CHALLENGE to self"); /* - * We return CHALLENGE here to prevent us from closing connection. Closing - * connection currently preempts remote end from reading any pending messages, + * We return the CHALLENGE here to prevent us from closing the connection. + * Closing the connection currently preempts the remote end from reading any pending messages, * specifically the CHALLENGE message we just sent above. When our 'remote' * outbound counterpart reads our message, they will close both connections. - * Failing that, our connection will timeout or a future handshake error will + * Failing that, our connection will time out or a future handshake error will * occur. */ return CHALLENGE; @@ -135,7 +135,7 @@ public enum Handshake { // Are we already connected to this peer? Peer existingPeer = Network.getInstance().getHandshakedPeerWithPublicKey(peersPublicKey); if (existingPeer != null) { - LOGGER.info(() -> String.format("We already have a connection with peer %s - discarding", peer)); + LOGGER.debug(() -> String.format("We already have a connection with peer %s - discarding", peer)); // Handshake failure - caller will deal with disconnect return null; } @@ -148,7 +148,7 @@ public enum Handshake { @Override public void action(Peer peer) { - // Send challenge + // Send a challenge byte[] publicKey = Network.getInstance().getOurPublicKey(); byte[] challenge = peer.getOurChallenge(); @@ -254,16 +254,17 @@ public enum Handshake { private static final Logger LOGGER = LogManager.getLogger(Handshake.class); - /** Maximum allowed difference between peer's reported timestamp and when they connected, in milliseconds. */ + /** The Maximum allowed difference between peer's reported timestamp and when they connected, in milliseconds. */ private static final long MAX_TIMESTAMP_DELTA = 30 * 1000L; // ms private static final long PEER_VERSION_131 = 0x0100030001L; /** Minimum peer version that we are allowed to communicate with */ - private static final String MIN_PEER_VERSION = "4.1.1"; + private static final String MIN_PEER_VERSION = "4.6.5"; private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits + // Can always be made harder in the future... private static final int POW_BUFFER_SIZE_POST_131 = 2 * 1024 * 1024; // bytes private static final int POW_DIFFICULTY_POST_131 = 2; // leading zero bits @@ -275,12 +276,11 @@ public enum Handshake { public final MessageType expectedMessageType; - private Handshake(MessageType expectedMessageType) { + Handshake(MessageType expectedMessageType) { this.expectedMessageType = expectedMessageType; } public abstract Handshake onMessage(Peer peer, Message message); public abstract void action(Peer peer); - } diff --git a/src/main/java/org/qortal/network/Network.java b/src/main/java/org/qortal/network/Network.java index f500b2e8..d8777eec 100644 --- a/src/main/java/org/qortal/network/Network.java +++ b/src/main/java/org/qortal/network/Network.java @@ -80,7 +80,7 @@ public class Network { "node.qortal.ru", "node2.qortal.ru", "node3.qortal.ru", "node.qortal.uk", "node22.qortal.org", "cinfu1.crowetic.com", "node.cwd.systems", "bootstrap.cwd.systems", "node1.qortalnodes.live", "node2.qortalnodes.live", "node3.qortalnodes.live", "node4.qortalnodes.live", "node5.qortalnodes.live", - "node6.qortalnodes.live", "node7.qortalnodes.live", "node8.qortalnodes.live" + "node.qortalnodes.live", "qortex.live", }; private static final long NETWORK_EPC_KEEPALIVE = 5L; // seconds @@ -149,7 +149,7 @@ public class Network { private final Lock mergePeersLock = new ReentrantLock(); - private List ourExternalIpAddressHistory = new ArrayList<>(); + private final List ourExternalIpAddressHistory = new ArrayList<>(); private String ourExternalIpAddress = null; private int ourExternalPort = Settings.getInstance().getListenPort(); @@ -167,7 +167,7 @@ public class Network { ExecutorService networkExecutor = new ThreadPoolExecutor(2, Settings.getInstance().getMaxNetworkThreadPoolSize(), NETWORK_EPC_KEEPALIVE, TimeUnit.SECONDS, - new SynchronousQueue(), + new SynchronousQueue<>(), new NamedThreadFactory("Network-EPC", Settings.getInstance().getNetworkThreadPriority())); networkEPC = new NetworkProcessor(networkExecutor); } @@ -314,7 +314,7 @@ public class Network { public List getImmutableConnectedDataPeers() { return this.getImmutableConnectedPeers().stream() - .filter(p -> p.isDataPeer()) + .filter(Peer::isDataPeer) .collect(Collectors.toList()); } @@ -346,7 +346,7 @@ public class Network { public boolean requestDataFromPeer(String peerAddressString, byte[] signature) { if (peerAddressString != null) { PeerAddress peerAddress = PeerAddress.fromString(peerAddressString); - PeerData peerData = null; + PeerData peerData; // Reuse an existing PeerData instance if it's already in the known peers list synchronized (this.allKnownPeers) { @@ -370,9 +370,9 @@ public class Network { // Check if we're already connected to and handshaked with this peer Peer connectedPeer = this.getImmutableConnectedPeers().stream() - .filter(p -> p.getPeerData().getAddress().equals(peerAddress)) - .findFirst() - .orElse(null); + .filter(p -> p.getPeerData().getAddress().equals(peerAddress)) + .findFirst() + .orElse(null); boolean isConnected = (connectedPeer != null); @@ -710,7 +710,7 @@ public class Network { return true; } - private Peer getConnectablePeer(final Long now) throws InterruptedException { + private Peer getConnectablePeer(final Long now) { // We can't block here so use tryRepository(). We don't NEED to connect a new peer. try (Repository repository = RepositoryManager.tryRepository()) { if (repository == null) { @@ -807,7 +807,7 @@ public class Network { // Find peers that have reached their maximum connection age, and disconnect them List peersToDisconnect = this.getImmutableConnectedPeers().stream() .filter(peer -> !peer.isSyncInProgress()) - .filter(peer -> peer.hasReachedMaxConnectionAge()) + .filter(Peer::hasReachedMaxConnectionAge) .collect(Collectors.toList()); if (peersToDisconnect != null && !peersToDisconnect.isEmpty()) { @@ -996,9 +996,9 @@ public class Network { } // Add to per-message thread count (first initializing to 0 if not already present) - threadsPerMessageType.computeIfAbsent(message.getType(), key -> 0); + threadsPerMessageType.putIfAbsent(message.getType(), 0); threadsPerMessageType.computeIfPresent(message.getType(), (key, value) -> value + 1); - + // Add to total thread count synchronized (this) { totalThreadCount++; @@ -1037,7 +1037,7 @@ public class Network { } // Remove from per-message thread count (first initializing to 0 if not already present) - threadsPerMessageType.computeIfAbsent(message.getType(), key -> 0); + threadsPerMessageType.putIfAbsent(message.getType(), 0); threadsPerMessageType.computeIfPresent(message.getType(), (key, value) -> value - 1); // Remove from total thread count @@ -1135,7 +1135,7 @@ public class Network { Peer existingPeer = getHandshakedPeerWithPublicKey(peer.getPeersPublicKey()); // NOTE: actual object reference compare, not Peer.equals() if (existingPeer != peer) { - LOGGER.info("[{}] We already have a connection with peer {} - discarding", + LOGGER.debug("[{}] We already have a connection with peer {} - discarding", peer.getPeerConnectionId(), peer); peer.disconnect("existing connection"); return; @@ -1216,29 +1216,7 @@ public class Network { * Returns PEERS message made from peers we've connected to recently, and this node's details */ public Message buildPeersMessage(Peer peer) { - List knownPeers = this.getAllKnownPeers(); - - // Filter out peers that we've not connected to ever or within X milliseconds - final long connectionThreshold = NTP.getTime() - RECENT_CONNECTION_THRESHOLD; - Predicate notRecentlyConnected = peerData -> { - final Long lastAttempted = peerData.getLastAttempted(); - final Long lastConnected = peerData.getLastConnected(); - - if (lastAttempted == null || lastConnected == null) { - return true; - } - - if (lastConnected < lastAttempted) { - return true; - } - - if (lastConnected < connectionThreshold) { - return true; - } - - return false; - }; - knownPeers.removeIf(notRecentlyConnected); + final var knownPeers = getPeerData(); List peerAddresses = new ArrayList<>(); @@ -1262,6 +1240,29 @@ public class Network { return new PeersV2Message(peerAddresses); } + private List getPeerData() { + List knownPeers = this.getAllKnownPeers(); + + // Filter out peers that we've not connected to ever or within X milliseconds + final long connectionThreshold = NTP.getTime() - RECENT_CONNECTION_THRESHOLD; + Predicate notRecentlyConnected = peerData -> { + final Long lastAttempted = peerData.getLastAttempted(); + final Long lastConnected = peerData.getLastConnected(); + + if (lastAttempted == null || lastConnected == null) { + return true; + } + + if (lastConnected < lastAttempted) { + return true; + } + + return lastConnected < connectionThreshold; + }; + knownPeers.removeIf(notRecentlyConnected); + return knownPeers; + } + /** Builds either (legacy) HeightV2Message or (newer) BlockSummariesV2Message, depending on peer version. * * @return Message, or null if DataException was thrown. @@ -1328,7 +1329,7 @@ public class Network { return; } String host = parts[0]; - + try { InetAddress addr = InetAddress.getByName(host); if (addr.isAnyLocalAddress() || addr.isSiteLocalAddress()) { @@ -1369,12 +1370,12 @@ public class Network { for (int i = size-1; i >= 0; i--) { String reading = ipAddressHistory.get(i); if (lastReading != null) { - if (Objects.equals(reading, lastReading)) { + if (Objects.equals(reading, lastReading)) { consecutiveReadings++; - } - else { - consecutiveReadings = 0; - } + } + else { + consecutiveReadings = 0; + } } lastReading = reading; } @@ -1515,12 +1516,8 @@ public class Network { return true; } - if (peerData.getLastConnected() == null - || peerData.getLastConnected() > now - OLD_PEER_CONNECTION_PERIOD) { - return true; - } - - return false; + return peerData.getLastConnected() == null + || peerData.getLastConnected() > now - OLD_PEER_CONNECTION_PERIOD; }; // Disregard peers that are NOT 'old' @@ -1655,7 +1652,7 @@ public class Network { // Stop processing threads try { - if (!this.networkEPC.shutdown(5000)) { + if (!this.networkEPC.shutdown(10000)) { LOGGER.warn("Network threads failed to terminate"); } } catch (InterruptedException e) { @@ -1667,5 +1664,4 @@ public class Network { peer.shutdown(); } } - } From e3a85786e7ebd7fa78f8bf711ee5493e136fc149 Mon Sep 17 00:00:00 2001 From: AlphaX-Qortal Date: Mon, 2 Dec 2024 15:06:46 +0100 Subject: [PATCH 08/16] Update dependencies --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index bde846a0..11c06bc0 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ 1.2.2 0.12.3 4.9.10 - 1.68.1 + 1.68.2 33.3.1-jre 2.2 1.2.1 @@ -35,7 +35,7 @@ 9.4.56.v20240826 1.1.1 20240303 - 1.18.1 + 1.18.3 5.11.0-M2 1.0.0 2.23.1 From 2e989aaa572b5ef30c7cf75c7c58290a82250661 Mon Sep 17 00:00:00 2001 From: crowetic Date: Tue, 3 Dec 2024 08:29:53 -0800 Subject: [PATCH 09/16] A merge of just alpha's validation changes, phil and quick's commits, and kenny's changes to test. --- src/main/java/org/qortal/account/Account.java | 20 ++- .../qortal/api/model/ApiOnlineAccount.java | 33 ++++ .../qortal/api/model/BlockMintingInfo.java | 2 +- .../qortal/api/resource/BlocksResource.java | 3 +- src/main/java/org/qortal/block/Block.java | 51 ++++-- .../java/org/qortal/data/block/BlockData.java | 29 +++- .../org/qortal/test/BlockArchiveV1Tests.java | 153 ++++++++++++++++- .../org/qortal/test/BlockArchiveV2Tests.java | 160 +++++++++++++++++- .../test-chain-v2-block-timestamps.json | 11 +- .../test-chain-v2-disable-reference.json | 11 +- .../test-chain-v2-founder-rewards.json | 11 +- .../test-chain-v2-leftover-reward.json | 11 +- src/test/resources/test-chain-v2-minting.json | 11 +- .../resources/test-chain-v2-penalty-fix.json | 14 +- .../test-chain-v2-qora-holder-extremes.json | 11 +- .../test-chain-v2-qora-holder-reduction.json | 11 +- .../resources/test-chain-v2-qora-holder.json | 11 +- .../test-chain-v2-reward-levels.json | 11 +- .../test-chain-v2-reward-scaling.json | 11 +- .../test-chain-v2-reward-shares.json | 11 +- ...est-chain-v2-self-sponsorship-algo-v1.json | 11 +- ...est-chain-v2-self-sponsorship-algo-v2.json | 11 +- ...est-chain-v2-self-sponsorship-algo-v3.json | 11 +- src/test/resources/test-chain-v2.json | 8 +- 24 files changed, 568 insertions(+), 59 deletions(-) diff --git a/src/main/java/org/qortal/account/Account.java b/src/main/java/org/qortal/account/Account.java index 537f0788..99fa5217 100644 --- a/src/main/java/org/qortal/account/Account.java +++ b/src/main/java/org/qortal/account/Account.java @@ -349,10 +349,28 @@ public class Account { } /** - * Returns 'effective' minting level, or zero if reward-share does not exist. + * Returns reward-share minting address, or unknown if reward-share does not exist. * * @param repository * @param rewardSharePublicKey + * @return address or unknown + * @throws DataException + */ + public static String getRewardShareMintingAddress(Repository repository, byte[] rewardSharePublicKey) throws DataException { + // Find actual minter address + RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(rewardSharePublicKey); + + if (rewardShareData == null) + return "Unknown"; + + return rewardShareData.getMinter(); + } + + /** + * Returns 'effective' minting level, or zero if reward-share does not exist. + * + * @param repository + * @param rewardSharePublicKey * @return 0+ * @throws DataException */ diff --git a/src/main/java/org/qortal/api/model/ApiOnlineAccount.java b/src/main/java/org/qortal/api/model/ApiOnlineAccount.java index 08b697aa..e26eb816 100644 --- a/src/main/java/org/qortal/api/model/ApiOnlineAccount.java +++ b/src/main/java/org/qortal/api/model/ApiOnlineAccount.java @@ -1,7 +1,13 @@ package org.qortal.api.model; +import org.qortal.account.Account; +import org.qortal.repository.DataException; +import org.qortal.repository.RepositoryManager; +import org.qortal.repository.Repository; + import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; // All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) @@ -47,4 +53,31 @@ public class ApiOnlineAccount { return this.recipientAddress; } + public int getMinterLevelFromPublicKey() { + try (final Repository repository = RepositoryManager.getRepository()) { + return Account.getRewardShareEffectiveMintingLevel(repository, this.rewardSharePublicKey); + } catch (DataException e) { + return 0; + } + } + + public boolean getIsMember() { + try (final Repository repository = RepositoryManager.getRepository()) { + return repository.getGroupRepository().memberExists(694, getMinterAddress()); + } catch (DataException e) { + return false; + } + } + + // JAXB special + + @XmlElement(name = "minterLevel") + protected int getMinterLevel() { + return getMinterLevelFromPublicKey(); + } + + @XmlElement(name = "isMinterMember") + protected boolean getMinterMember() { + return getIsMember(); + } } diff --git a/src/main/java/org/qortal/api/model/BlockMintingInfo.java b/src/main/java/org/qortal/api/model/BlockMintingInfo.java index f84e179e..02765a89 100644 --- a/src/main/java/org/qortal/api/model/BlockMintingInfo.java +++ b/src/main/java/org/qortal/api/model/BlockMintingInfo.java @@ -9,6 +9,7 @@ import java.math.BigInteger; public class BlockMintingInfo { public byte[] minterPublicKey; + public String minterAddress; public int minterLevel; public int onlineAccountsCount; public BigDecimal maxDistance; @@ -19,5 +20,4 @@ public class BlockMintingInfo { public BlockMintingInfo() { } - } diff --git a/src/main/java/org/qortal/api/resource/BlocksResource.java b/src/main/java/org/qortal/api/resource/BlocksResource.java index 01d8d2ab..ff0bb979 100644 --- a/src/main/java/org/qortal/api/resource/BlocksResource.java +++ b/src/main/java/org/qortal/api/resource/BlocksResource.java @@ -542,6 +542,7 @@ public class BlocksResource { } } + String minterAddress = Account.getRewardShareMintingAddress(repository, blockData.getMinterPublicKey()); int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey()); if (minterLevel == 0) // This may be unavailable when requesting a trimmed block @@ -554,6 +555,7 @@ public class BlocksResource { BlockMintingInfo blockMintingInfo = new BlockMintingInfo(); blockMintingInfo.minterPublicKey = blockData.getMinterPublicKey(); + blockMintingInfo.minterAddress = minterAddress; blockMintingInfo.minterLevel = minterLevel; blockMintingInfo.onlineAccountsCount = blockData.getOnlineAccountsCount(); blockMintingInfo.maxDistance = new BigDecimal(block.MAX_DISTANCE); @@ -887,5 +889,4 @@ public class BlocksResource { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); } } - } diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 918a20ae..c9353d70 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -145,7 +145,7 @@ public class Block { private final Account recipientAccount; private final AccountData recipientAccountData; - + final BlockChain blockChain = BlockChain.getInstance(); ExpandedAccount(Repository repository, RewardShareData rewardShareData) throws DataException { @@ -414,6 +414,21 @@ public class Block { }); } + // After feature trigger, remove any online accounts that are not minter group member + if (height >= BlockChain.getInstance().getGroupMemberCheckHeight()) { + onlineAccounts.removeIf(a -> { + try { + int groupId = BlockChain.getInstance().getMintingGroupId(); + String address = Account.getRewardShareMintingAddress(repository, a.getPublicKey()); + boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address); + return !isMinterGroupMember; + } catch (DataException e) { + // Something went wrong, so remove the account + return true; + } + }); + } + if (onlineAccounts.isEmpty()) { LOGGER.debug("No online accounts - not even our own?"); return null; @@ -721,19 +736,19 @@ public class Block { List expandedAccounts = new ArrayList<>(); for (RewardShareData rewardShare : this.cachedOnlineRewardShares) { - if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) { + int groupId = BlockChain.getInstance().getMintingGroupId(); + String address = rewardShare.getMinter(); + boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address); + + if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) + expandedAccounts.add(new ExpandedAccount(repository, rewardShare)); + + if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight() && isMinterGroupMember) expandedAccounts.add(new ExpandedAccount(repository, rewardShare)); - } - if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight()) { - boolean isMinterGroupMember = repository.getGroupRepository().memberExists(BlockChain.getInstance().getMintingGroupId(), rewardShare.getMinter()); - if (isMinterGroupMember) { - expandedAccounts.add(new ExpandedAccount(repository, rewardShare)); - } - } } - this.cachedExpandedAccounts = expandedAccounts; + LOGGER.trace(() -> String.format("Online reward-shares after expanded accounts %s", this.cachedOnlineRewardShares)); return this.cachedExpandedAccounts; } @@ -1143,8 +1158,17 @@ public class Block { if (this.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) { List expandedAccounts = this.getExpandedAccounts(); for (ExpandedAccount account : expandedAccounts) { + int groupId = BlockChain.getInstance().getMintingGroupId(); + String address = account.getMintingAccount().getAddress(); + boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address); + if (account.getMintingAccount().getEffectiveMintingLevel() == 0) return ValidationResult.ONLINE_ACCOUNTS_INVALID; + + if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight()) { + if (!isMinterGroupMember) + return ValidationResult.ONLINE_ACCOUNTS_INVALID; + } } } @@ -1273,6 +1297,7 @@ public class Block { // Online Accounts ValidationResult onlineAccountsResult = this.areOnlineAccountsValid(); + LOGGER.trace("Accounts valid = {}", onlineAccountsResult); if (onlineAccountsResult != ValidationResult.OK) return onlineAccountsResult; @@ -1361,7 +1386,7 @@ public class Block { // Check transaction can even be processed validationResult = transaction.isProcessable(); if (validationResult != Transaction.ValidationResult.OK) { - LOGGER.info(String.format("Error during transaction validation, tx %s: %s", Base58.encode(transactionData.getSignature()), validationResult.name())); + LOGGER.debug(String.format("Error during transaction validation, tx %s: %s", Base58.encode(transactionData.getSignature()), validationResult.name())); return ValidationResult.TRANSACTION_INVALID; } @@ -1562,6 +1587,7 @@ public class Block { this.blockData.setHeight(blockchainHeight + 1); LOGGER.trace(() -> String.format("Processing block %d", this.blockData.getHeight())); + LOGGER.trace(() -> String.format("Online Reward Shares in process %s", this.cachedOnlineRewardShares)); if (this.blockData.getHeight() > 1) { @@ -2280,7 +2306,6 @@ public class Block { // Select the correct set of share bins based on block height List accountLevelShareBinsForBlock = (this.blockData.getHeight() >= BlockChain.getInstance().getSharesByLevelV2Height()) ? BlockChain.getInstance().getAccountLevelShareBinsV2() : BlockChain.getInstance().getAccountLevelShareBinsV1(); - // Determine reward candidates based on account level // This needs a deep copy, so the shares can be modified when tiers aren't activated yet List accountLevelShareBins = new ArrayList<>(); @@ -2570,9 +2595,11 @@ public class Block { return; int minterLevel = Account.getRewardShareEffectiveMintingLevel(this.repository, this.getMinter().getPublicKey()); + String minterAddress = Account.getRewardShareMintingAddress(this.repository, this.getMinter().getPublicKey()); LOGGER.debug(String.format("======= BLOCK %d (%.8s) =======", this.getBlockData().getHeight(), Base58.encode(this.getSignature()))); LOGGER.debug(String.format("Timestamp: %d", this.getBlockData().getTimestamp())); + LOGGER.debug(String.format("Minter address: %s", minterAddress)); LOGGER.debug(String.format("Minter level: %d", minterLevel)); LOGGER.debug(String.format("Online accounts: %d", this.getBlockData().getOnlineAccountsCount())); LOGGER.debug(String.format("AT count: %d", this.getBlockData().getATCount())); diff --git a/src/main/java/org/qortal/data/block/BlockData.java b/src/main/java/org/qortal/data/block/BlockData.java index 34df0f9a..7e2a1872 100644 --- a/src/main/java/org/qortal/data/block/BlockData.java +++ b/src/main/java/org/qortal/data/block/BlockData.java @@ -1,8 +1,11 @@ package org.qortal.data.block; import com.google.common.primitives.Bytes; +import org.qortal.account.Account; import org.qortal.block.BlockChain; -import org.qortal.crypto.Crypto; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; import org.qortal.settings.Settings; import org.qortal.utils.NTP; @@ -224,7 +227,7 @@ public class BlockData implements Serializable { } return 0; } - + public boolean isTrimmed() { long onlineAccountSignaturesTrimmedTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime(); long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime(); @@ -232,11 +235,31 @@ public class BlockData implements Serializable { return blockTimestamp < onlineAccountSignaturesTrimmedTimestamp && blockTimestamp < currentTrimmableTimestamp; } + public String getMinterAddressFromPublicKey() { + try (final Repository repository = RepositoryManager.getRepository()) { + return Account.getRewardShareMintingAddress(repository, this.minterPublicKey); + } catch (DataException e) { + return "Unknown"; + } + } + + public int getMinterLevelFromPublicKey() { + try (final Repository repository = RepositoryManager.getRepository()) { + return Account.getRewardShareEffectiveMintingLevel(repository, this.minterPublicKey); + } catch (DataException e) { + return 0; + } + } + // JAXB special @XmlElement(name = "minterAddress") protected String getMinterAddress() { - return Crypto.toAddress(this.minterPublicKey); + return getMinterAddressFromPublicKey(); } + @XmlElement(name = "minterLevel") + protected int getMinterLevel() { + return getMinterLevelFromPublicKey(); + } } diff --git a/src/test/java/org/qortal/test/BlockArchiveV1Tests.java b/src/test/java/org/qortal/test/BlockArchiveV1Tests.java index a28bd28d..2cf8ef79 100644 --- a/src/test/java/org/qortal/test/BlockArchiveV1Tests.java +++ b/src/test/java/org/qortal/test/BlockArchiveV1Tests.java @@ -54,26 +54,39 @@ public class BlockArchiveV1Tests extends Common { public void testWriter() throws DataException, InterruptedException, TransformationException, IOException { try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Starting testWriter"); + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); for (int i = 0; i < 1000; i++) { BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } } + System.out.println("Finished minting blocks."); // 900 blocks are trimmed (this specifies the first untrimmed height) repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); // Check the max archive height - this should be one less than the first untrimmed height final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); assertEquals(900, maximumArchiveHeight); // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); assertEquals(900 - 1, writer.getWrittenCount()); // Increment block archive height @@ -84,6 +97,9 @@ public class BlockArchiveV1Tests extends Common { // Ensure the file exists File outputFile = writer.getOutputPath().toFile(); assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + + System.out.println("testWriter completed successfully."); } } @@ -91,26 +107,39 @@ public class BlockArchiveV1Tests extends Common { public void testWriterAndReader() throws DataException, InterruptedException, TransformationException, IOException { try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Starting testWriterAndReader"); + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); for (int i = 0; i < 1000; i++) { BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } } + System.out.println("Finished minting blocks."); // 900 blocks are trimmed (this specifies the first untrimmed height) repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); // Check the max archive height - this should be one less than the first untrimmed height final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); assertEquals(900, maximumArchiveHeight); // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); assertEquals(900 - 1, writer.getWrittenCount()); // Increment block archive height @@ -121,8 +150,10 @@ public class BlockArchiveV1Tests extends Common { // Ensure the file exists File outputFile = writer.getOutputPath().toFile(); assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); // Read block 2 from the archive + System.out.println("Reading block 2 from the archive..."); BlockArchiveReader reader = BlockArchiveReader.getInstance(); BlockTransformation block2Info = reader.fetchBlockAtHeight(2); BlockData block2ArchiveData = block2Info.getBlockData(); @@ -131,6 +162,7 @@ public class BlockArchiveV1Tests extends Common { BlockData block2RepositoryData = repository.getBlockRepository().fromHeight(2); // Ensure the values match + System.out.println("Comparing block 2 data..."); assertEquals(block2ArchiveData.getHeight(), block2RepositoryData.getHeight()); assertArrayEquals(block2ArchiveData.getSignature(), block2RepositoryData.getSignature()); @@ -138,6 +170,7 @@ public class BlockArchiveV1Tests extends Common { assertEquals(1, block2ArchiveData.getOnlineAccountsCount()); // Read block 900 from the archive + System.out.println("Reading block 900 from the archive..."); BlockTransformation block900Info = reader.fetchBlockAtHeight(900); BlockData block900ArchiveData = block900Info.getBlockData(); @@ -145,12 +178,14 @@ public class BlockArchiveV1Tests extends Common { BlockData block900RepositoryData = repository.getBlockRepository().fromHeight(900); // Ensure the values match + System.out.println("Comparing block 900 data..."); assertEquals(block900ArchiveData.getHeight(), block900RepositoryData.getHeight()); assertArrayEquals(block900ArchiveData.getSignature(), block900RepositoryData.getSignature()); // Test some values in the archive assertEquals(1, block900ArchiveData.getOnlineAccountsCount()); + System.out.println("testWriterAndReader completed successfully."); } } @@ -158,33 +193,48 @@ public class BlockArchiveV1Tests extends Common { public void testArchivedAtStates() throws DataException, InterruptedException, TransformationException, IOException { try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Starting testArchivedAtStates"); + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); byte[] creationBytes = AtUtils.buildSimpleAT(); long fundingAmount = 1_00000000L; DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); String atAddress = deployAtTransaction.getATAccount().getAddress(); + System.out.println("AT deployed at address: " + atAddress); // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); for (int i = 0; i < 1000; i++) { BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } } + System.out.println("Finished minting blocks."); // 9 blocks are trimmed (this specifies the first untrimmed height) repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(10); repository.getATRepository().setAtTrimHeight(10); + System.out.println("Set trim heights to 10."); // Check the max archive height final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 9): " + maximumArchiveHeight); assertEquals(9, maximumArchiveHeight); // Write blocks 2-9 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 8)"); assertEquals(9 - 1, writer.getWrittenCount()); // Increment block archive height @@ -195,10 +245,13 @@ public class BlockArchiveV1Tests extends Common { // Ensure the file exists File outputFile = writer.getOutputPath().toFile(); assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); // Check blocks 3-9 + System.out.println("Checking blocks 3 to 9..."); for (Integer testHeight = 2; testHeight <= 9; testHeight++) { + System.out.println("Reading block " + testHeight + " from the archive..."); // Read a block from the archive BlockArchiveReader reader = BlockArchiveReader.getInstance(); BlockTransformation blockInfo = reader.fetchBlockAtHeight(testHeight); @@ -216,6 +269,7 @@ public class BlockArchiveV1Tests extends Common { // Check the archived AT state if (testHeight == 2) { + System.out.println("Checking block " + testHeight + " AT state data (expected null)..."); // Block 2 won't have an AT state hash because it's initial (and has the DEPLOY_AT in the same block) assertNull(archivedAtStateData); @@ -223,6 +277,7 @@ public class BlockArchiveV1Tests extends Common { assertEquals(Transaction.TransactionType.DEPLOY_AT, archivedTransactions.get(0).getType()); } else { + System.out.println("Checking block " + testHeight + " AT state data..."); // For blocks 3+, ensure the archive has the AT state data, but not the hashes assertNotNull(archivedAtStateData.getStateHash()); assertNull(archivedAtStateData.getStateData()); @@ -255,10 +310,12 @@ public class BlockArchiveV1Tests extends Common { } // Check block 10 (unarchived) + System.out.println("Checking block 10 (should not be in archive)..."); BlockArchiveReader reader = BlockArchiveReader.getInstance(); BlockTransformation blockInfo = reader.fetchBlockAtHeight(10); assertNull(blockInfo); + System.out.println("testArchivedAtStates completed successfully."); } } @@ -267,32 +324,46 @@ public class BlockArchiveV1Tests extends Common { public void testArchiveAndPrune() throws DataException, InterruptedException, TransformationException, IOException { try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Starting testArchiveAndPrune"); + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); byte[] creationBytes = AtUtils.buildSimpleAT(); long fundingAmount = 1_00000000L; AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); for (int i = 0; i < 1000; i++) { BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } } + System.out.println("Finished minting blocks."); // Assume 900 blocks are trimmed (this specifies the first untrimmed height) repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); // Check the max archive height - this should be one less than the first untrimmed height final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); assertEquals(900, maximumArchiveHeight); // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); assertEquals(900 - 1, writer.getWrittenCount()); // Increment block archive height @@ -303,17 +374,21 @@ public class BlockArchiveV1Tests extends Common { // Ensure the file exists File outputFile = writer.getOutputPath().toFile(); assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); // Ensure the SQL repository contains blocks 2 and 900... assertNotNull(repository.getBlockRepository().fromHeight(2)); assertNotNull(repository.getBlockRepository().fromHeight(900)); + System.out.println("Blocks 2 and 900 exist in the repository."); // Prune all the archived blocks + System.out.println("Pruning blocks 2 to 900..."); int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 900); assertEquals(900-1, numBlocksPruned); repository.getBlockRepository().setBlockPruneHeight(901); // Prune the AT states for the archived blocks + System.out.println("Pruning AT states up to height 900..."); repository.getATRepository().rebuildLatestAtStates(900); repository.saveChanges(); int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 900); @@ -323,14 +398,19 @@ public class BlockArchiveV1Tests extends Common { // Now ensure the SQL repository is missing blocks 2 and 900... assertNull(repository.getBlockRepository().fromHeight(2)); assertNull(repository.getBlockRepository().fromHeight(900)); + System.out.println("Blocks 2 and 900 have been pruned from the repository."); // ... but it's not missing blocks 1 and 901 (we don't prune the genesis block) assertNotNull(repository.getBlockRepository().fromHeight(1)); assertNotNull(repository.getBlockRepository().fromHeight(901)); + System.out.println("Blocks 1 and 901 still exist in the repository."); // Validate the latest block height in the repository - assertEquals(1002, (int) repository.getBlockRepository().getLastBlock().getHeight()); + int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); + assertEquals(1002, lastBlockHeight); + System.out.println("testArchiveAndPrune completed successfully."); } } @@ -338,137 +418,190 @@ public class BlockArchiveV1Tests extends Common { public void testTrimArchivePruneAndOrphan() throws DataException, InterruptedException, TransformationException, IOException { try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Starting testTrimArchivePruneAndOrphan"); + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); byte[] creationBytes = AtUtils.buildSimpleAT(); long fundingAmount = 1_00000000L; AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + System.out.println("AT deployed successfully."); // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); for (int i = 0; i < 1000; i++) { BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } } + System.out.println("Finished minting blocks."); // Make sure that block 500 has full AT state data and data hash + System.out.println("Verifying block 500 AT state data..."); List block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); ATStateData atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); assertNotNull(atStatesData.getStateHash()); assertNotNull(atStatesData.getStateData()); + System.out.println("Block 500 AT state data verified."); // Trim the first 500 blocks + System.out.println("Trimming first 500 blocks..."); repository.getBlockRepository().trimOldOnlineAccountsSignatures(0, 500); repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(501); repository.getATRepository().rebuildLatestAtStates(500); repository.getATRepository().trimAtStates(0, 500, 1000); repository.getATRepository().setAtTrimHeight(501); + System.out.println("Trimming completed."); // Now block 499 should only have the AT state data hash + System.out.println("Checking block 499 AT state data..."); List block499AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(499); atStatesData = repository.getATRepository().getATStateAtHeight(block499AtStatesData.get(0).getATAddress(), 499); assertNotNull(atStatesData.getStateHash()); assertNull(atStatesData.getStateData()); + System.out.println("Block 499 AT state data contains only state hash as expected."); // ... but block 500 should have the full data (due to being retained as the "latest" AT state in the trimmed range + System.out.println("Verifying block 500 AT state data again..."); block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); assertNotNull(atStatesData.getStateHash()); assertNotNull(atStatesData.getStateData()); + System.out.println("Block 500 AT state data contains full data."); // ... and block 501 should also have the full data + System.out.println("Verifying block 501 AT state data..."); List block501AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(501); atStatesData = repository.getATRepository().getATStateAtHeight(block501AtStatesData.get(0).getATAddress(), 501); assertNotNull(atStatesData.getStateHash()); assertNotNull(atStatesData.getStateData()); + System.out.println("Block 501 AT state data contains full data."); // Check the max archive height - this should be one less than the first untrimmed height final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height determined (Expected 500): " + maximumArchiveHeight); assertEquals(500, maximumArchiveHeight); BlockData block3DataPreArchive = repository.getBlockRepository().fromHeight(3); // Write blocks 2-500 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); // Make sure that the archive contains the correct number of blocks + System.out.println("Number of blocks written to archive (Expected 499): " + writer.getWrittenCount()); assertEquals(500 - 1, writer.getWrittenCount()); // -1 for the genesis block // Increment block archive height repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); repository.saveChanges(); assertEquals(500 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + System.out.println("Block archive height updated to: " + (500 - 1)); // Ensure the file exists File outputFile = writer.getOutputPath().toFile(); assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); // Ensure the SQL repository contains blocks 2 and 500... + System.out.println("Verifying that blocks 2 and 500 exist in the repository..."); assertNotNull(repository.getBlockRepository().fromHeight(2)); assertNotNull(repository.getBlockRepository().fromHeight(500)); + System.out.println("Blocks 2 and 500 are present in the repository."); // Prune all the archived blocks + System.out.println("Pruning blocks 2 to 500..."); int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 500); + System.out.println("Number of blocks pruned (Expected 499): " + numBlocksPruned); assertEquals(500-1, numBlocksPruned); repository.getBlockRepository().setBlockPruneHeight(501); // Prune the AT states for the archived blocks + System.out.println("Pruning AT states up to height 500..."); repository.getATRepository().rebuildLatestAtStates(500); repository.saveChanges(); int numATStatesPruned = repository.getATRepository().pruneAtStates(2, 500); + System.out.println("Number of AT states pruned (Expected 498): " + numATStatesPruned); assertEquals(498, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state repository.getATRepository().setAtPruneHeight(501); // Now ensure the SQL repository is missing blocks 2 and 500... + System.out.println("Verifying that blocks 2 and 500 have been pruned..."); assertNull(repository.getBlockRepository().fromHeight(2)); assertNull(repository.getBlockRepository().fromHeight(500)); + System.out.println("Blocks 2 and 500 have been successfully pruned."); // ... but it's not missing blocks 1 and 501 (we don't prune the genesis block) + System.out.println("Verifying that blocks 1 and 501 still exist..."); assertNotNull(repository.getBlockRepository().fromHeight(1)); assertNotNull(repository.getBlockRepository().fromHeight(501)); + System.out.println("Blocks 1 and 501 are present in the repository."); // Validate the latest block height in the repository - assertEquals(1002, (int) repository.getBlockRepository().getLastBlock().getHeight()); + int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); + assertEquals(1002, lastBlockHeight); // Now orphan some unarchived blocks. + System.out.println("Orphaning 500 blocks..."); BlockUtils.orphanBlocks(repository, 500); - assertEquals(502, (int) repository.getBlockRepository().getLastBlock().getHeight()); + int currentLastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("New last block height after orphaning (Expected 502): " + currentLastBlockHeight); + assertEquals(502, currentLastBlockHeight); // We're close to the lower limit of the SQL database now, so // we need to import some blocks from the archive + System.out.println("Importing blocks 401 to 500 from the archive..."); BlockArchiveUtils.importFromArchive(401, 500, repository); // Ensure the SQL repository now contains block 401 but not 400... + System.out.println("Verifying that block 401 exists and block 400 does not..."); assertNotNull(repository.getBlockRepository().fromHeight(401)); assertNull(repository.getBlockRepository().fromHeight(400)); + System.out.println("Block 401 exists, block 400 does not."); // Import the remaining 399 blocks + System.out.println("Importing blocks 2 to 400 from the archive..."); BlockArchiveUtils.importFromArchive(2, 400, repository); // Verify that block 3 matches the original + System.out.println("Verifying that block 3 matches the original data..."); BlockData block3DataPostArchive = repository.getBlockRepository().fromHeight(3); assertArrayEquals(block3DataPreArchive.getSignature(), block3DataPostArchive.getSignature()); assertEquals(block3DataPreArchive.getHeight(), block3DataPostArchive.getHeight()); + System.out.println("Block 3 data matches the original."); // Orphan 1 more block, which should be the last one that is possible to be orphaned + System.out.println("Orphaning 1 more block..."); BlockUtils.orphanBlocks(repository, 1); + System.out.println("Orphaned 1 block successfully."); // Orphan another block, which should fail + System.out.println("Attempting to orphan another block, which should fail..."); Exception exception = null; try { BlockUtils.orphanBlocks(repository, 1); } catch (DataException e) { exception = e; + System.out.println("Caught expected DataException: " + e.getMessage()); } // Ensure that a DataException is thrown because there is no more AT states data available assertNotNull(exception); assertEquals(DataException.class, exception.getClass()); + System.out.println("DataException confirmed due to lack of AT states data."); // FUTURE: we may be able to retain unique AT states when trimming, to avoid this exception // and allow orphaning back through blocks with trimmed AT states. + System.out.println("testTrimArchivePruneAndOrphan completed successfully."); } } @@ -482,16 +615,26 @@ public class BlockArchiveV1Tests extends Common { public void testMissingAtStatesHeightIndex() throws DataException, SQLException { try (final HSQLDBRepository repository = (HSQLDBRepository) RepositoryManager.getRepository()) { + System.out.println("Starting testMissingAtStatesHeightIndex"); + // Firstly check that we're able to prune or archive when the index exists + System.out.println("Checking existence of ATStatesHeightIndex..."); assertTrue(repository.getATRepository().hasAtStatesHeightIndex()); assertTrue(RepositoryManager.canArchiveOrPrune()); + System.out.println("ATStatesHeightIndex exists. Archiving and pruning are possible."); // Delete the index + System.out.println("Dropping ATStatesHeightIndex..."); repository.prepareStatement("DROP INDEX ATSTATESHEIGHTINDEX").execute(); + System.out.println("ATStatesHeightIndex dropped."); // Ensure check that we're unable to prune or archive when the index doesn't exist + System.out.println("Verifying that ATStatesHeightIndex no longer exists..."); assertFalse(repository.getATRepository().hasAtStatesHeightIndex()); assertFalse(RepositoryManager.canArchiveOrPrune()); + System.out.println("ATStatesHeightIndex does not exist. Archiving and pruning are disabled."); + + System.out.println("testMissingAtStatesHeightIndex completed successfully."); } } @@ -501,8 +644,10 @@ public class BlockArchiveV1Tests extends Common { Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toAbsolutePath(); try { FileUtils.deleteDirectory(archivePath.toFile()); + System.out.println("Deleted archive directory at: " + archivePath); } catch (IOException e) { - + + System.out.println("Failed to delete archive directory: " + e.getMessage()); } } diff --git a/src/test/java/org/qortal/test/BlockArchiveV2Tests.java b/src/test/java/org/qortal/test/BlockArchiveV2Tests.java index 3b1d12d3..8ab02b40 100644 --- a/src/test/java/org/qortal/test/BlockArchiveV2Tests.java +++ b/src/test/java/org/qortal/test/BlockArchiveV2Tests.java @@ -54,26 +54,39 @@ public class BlockArchiveV2Tests extends Common { public void testWriter() throws DataException, InterruptedException, TransformationException, IOException { try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Starting testWriter"); + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); for (int i = 0; i < 1000; i++) { BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } } + System.out.println("Finished minting blocks."); // 900 blocks are trimmed (this specifies the first untrimmed height) repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); // Check the max archive height - this should be one less than the first untrimmed height final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); assertEquals(900, maximumArchiveHeight); // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); assertEquals(900 - 1, writer.getWrittenCount()); // Increment block archive height @@ -84,6 +97,9 @@ public class BlockArchiveV2Tests extends Common { // Ensure the file exists File outputFile = writer.getOutputPath().toFile(); assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); + + System.out.println("testWriter completed successfully."); } } @@ -91,26 +107,39 @@ public class BlockArchiveV2Tests extends Common { public void testWriterAndReader() throws DataException, InterruptedException, TransformationException, IOException { try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Starting testWriterAndReader"); + // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); for (int i = 0; i < 1000; i++) { BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } } + System.out.println("Finished minting blocks."); // 900 blocks are trimmed (this specifies the first untrimmed height) repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); // Check the max archive height - this should be one less than the first untrimmed height final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); assertEquals(900, maximumArchiveHeight); // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); assertEquals(900 - 1, writer.getWrittenCount()); // Increment block archive height @@ -121,8 +150,10 @@ public class BlockArchiveV2Tests extends Common { // Ensure the file exists File outputFile = writer.getOutputPath().toFile(); assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); // Read block 2 from the archive + System.out.println("Reading block 2 from the archive..."); BlockArchiveReader reader = BlockArchiveReader.getInstance(); BlockTransformation block2Info = reader.fetchBlockAtHeight(2); BlockData block2ArchiveData = block2Info.getBlockData(); @@ -131,6 +162,7 @@ public class BlockArchiveV2Tests extends Common { BlockData block2RepositoryData = repository.getBlockRepository().fromHeight(2); // Ensure the values match + System.out.println("Comparing block 2 data..."); assertEquals(block2ArchiveData.getHeight(), block2RepositoryData.getHeight()); assertArrayEquals(block2ArchiveData.getSignature(), block2RepositoryData.getSignature()); @@ -138,6 +170,7 @@ public class BlockArchiveV2Tests extends Common { assertEquals(1, block2ArchiveData.getOnlineAccountsCount()); // Read block 900 from the archive + System.out.println("Reading block 900 from the archive..."); BlockTransformation block900Info = reader.fetchBlockAtHeight(900); BlockData block900ArchiveData = block900Info.getBlockData(); @@ -145,12 +178,14 @@ public class BlockArchiveV2Tests extends Common { BlockData block900RepositoryData = repository.getBlockRepository().fromHeight(900); // Ensure the values match + System.out.println("Comparing block 900 data..."); assertEquals(block900ArchiveData.getHeight(), block900RepositoryData.getHeight()); assertArrayEquals(block900ArchiveData.getSignature(), block900RepositoryData.getSignature()); // Test some values in the archive assertEquals(1, block900ArchiveData.getOnlineAccountsCount()); + System.out.println("testWriterAndReader completed successfully."); } } @@ -158,47 +193,66 @@ public class BlockArchiveV2Tests extends Common { public void testArchivedAtStates() throws DataException, InterruptedException, TransformationException, IOException { try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Starting testArchivedAtStates"); + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); byte[] creationBytes = AtUtils.buildSimpleAT(); long fundingAmount = 1_00000000L; DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); String atAddress = deployAtTransaction.getATAccount().getAddress(); + System.out.println("AT deployed at address: " + atAddress); // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); for (int i = 0; i < 1000; i++) { BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } } + System.out.println("Finished minting blocks."); // 9 blocks are trimmed (this specifies the first untrimmed height) repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(10); repository.getATRepository().setAtTrimHeight(10); + System.out.println("Set trim heights to 10."); // Check the max archive height final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 9): " + maximumArchiveHeight); assertEquals(9, maximumArchiveHeight); // Write blocks 2-9 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 8)"); assertEquals(9 - 1, writer.getWrittenCount()); // Increment block archive height repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); repository.saveChanges(); assertEquals(9 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + System.out.println("Block archive height updated to: " + (9 - 1)); // Ensure the file exists File outputFile = writer.getOutputPath().toFile(); assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); // Check blocks 3-9 + System.out.println("Checking blocks 2 to 9..."); for (Integer testHeight = 2; testHeight <= 9; testHeight++) { + System.out.println("Reading block " + testHeight + " from the archive..."); // Read a block from the archive BlockArchiveReader reader = BlockArchiveReader.getInstance(); BlockTransformation blockInfo = reader.fetchBlockAtHeight(testHeight); @@ -216,15 +270,18 @@ public class BlockArchiveV2Tests extends Common { // Check the archived AT state if (testHeight == 2) { + System.out.println("Checking block " + testHeight + " AT state data (expected transactions)..."); assertEquals(1, archivedTransactions.size()); assertEquals(Transaction.TransactionType.DEPLOY_AT, archivedTransactions.get(0).getType()); } else { + System.out.println("Checking block " + testHeight + " AT state data (no transactions expected)..."); // Blocks 3+ shouldn't have any transactions assertTrue(archivedTransactions.isEmpty()); } // Ensure the archive has the AT states hash + System.out.println("Checking block " + testHeight + " AT states hash..."); assertNotNull(archivedAtStateHash); // Also check the online accounts count and height @@ -232,6 +289,7 @@ public class BlockArchiveV2Tests extends Common { assertEquals(testHeight, archivedBlockData.getHeight()); // Ensure the values match + System.out.println("Comparing block " + testHeight + " data..."); assertEquals(archivedBlockData.getHeight(), repositoryBlockData.getHeight()); assertArrayEquals(archivedBlockData.getSignature(), repositoryBlockData.getSignature()); assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount()); @@ -249,10 +307,12 @@ public class BlockArchiveV2Tests extends Common { } // Check block 10 (unarchived) + System.out.println("Checking block 10 (should not be in archive)..."); BlockArchiveReader reader = BlockArchiveReader.getInstance(); BlockTransformation blockInfo = reader.fetchBlockAtHeight(10); assertNull(blockInfo); + System.out.println("testArchivedAtStates completed successfully."); } } @@ -261,32 +321,47 @@ public class BlockArchiveV2Tests extends Common { public void testArchiveAndPrune() throws DataException, InterruptedException, TransformationException, IOException { try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Starting testArchiveAndPrune"); + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); byte[] creationBytes = AtUtils.buildSimpleAT(); long fundingAmount = 1_00000000L; AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + System.out.println("AT deployed successfully."); // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); for (int i = 0; i < 1000; i++) { BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } } + System.out.println("Finished minting blocks."); // Assume 900 blocks are trimmed (this specifies the first untrimmed height) repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901); repository.getATRepository().setAtTrimHeight(901); + System.out.println("Set trim heights to 901."); // Check the max archive height - this should be one less than the first untrimmed height final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height (Expected 900): " + maximumArchiveHeight); assertEquals(900, maximumArchiveHeight); // Write blocks 2-900 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); // Make sure that the archive contains the correct number of blocks + System.out.println("Archive contains " + writer.getWrittenCount() + " blocks. (Expected 899)"); assertEquals(900 - 1, writer.getWrittenCount()); // Increment block archive height @@ -297,34 +372,48 @@ public class BlockArchiveV2Tests extends Common { // Ensure the file exists File outputFile = writer.getOutputPath().toFile(); assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); // Ensure the SQL repository contains blocks 2 and 900... + System.out.println("Verifying that blocks 2 and 900 exist in the repository..."); assertNotNull(repository.getBlockRepository().fromHeight(2)); assertNotNull(repository.getBlockRepository().fromHeight(900)); + System.out.println("Blocks 2 and 900 are present in the repository."); // Prune all the archived blocks + System.out.println("Pruning blocks 2 to 900..."); int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 900); + System.out.println("Number of blocks pruned (Expected 899): " + numBlocksPruned); assertEquals(900-1, numBlocksPruned); repository.getBlockRepository().setBlockPruneHeight(901); // Prune the AT states for the archived blocks + System.out.println("Pruning AT states up to height 900..."); repository.getATRepository().rebuildLatestAtStates(900); repository.saveChanges(); int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 900); + System.out.println("Number of AT states pruned (Expected 898): " + numATStatesPruned); assertEquals(900-2, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state repository.getATRepository().setAtPruneHeight(901); // Now ensure the SQL repository is missing blocks 2 and 900... + System.out.println("Verifying that blocks 2 and 900 have been pruned..."); assertNull(repository.getBlockRepository().fromHeight(2)); assertNull(repository.getBlockRepository().fromHeight(900)); + System.out.println("Blocks 2 and 900 have been successfully pruned."); // ... but it's not missing blocks 1 and 901 (we don't prune the genesis block) + System.out.println("Verifying that blocks 1 and 901 still exist..."); assertNotNull(repository.getBlockRepository().fromHeight(1)); assertNotNull(repository.getBlockRepository().fromHeight(901)); + System.out.println("Blocks 1 and 901 are present in the repository."); // Validate the latest block height in the repository - assertEquals(1002, (int) repository.getBlockRepository().getLastBlock().getHeight()); + int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); + assertEquals(1002, lastBlockHeight); + System.out.println("testArchiveAndPrune completed successfully."); } } @@ -332,138 +421,191 @@ public class BlockArchiveV2Tests extends Common { public void testTrimArchivePruneAndOrphan() throws DataException, InterruptedException, TransformationException, IOException { try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Starting testTrimArchivePruneAndOrphan"); + // Deploy an AT so that we have AT state data + System.out.println("Deploying AT..."); PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); byte[] creationBytes = AtUtils.buildSimpleAT(); long fundingAmount = 1_00000000L; AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount); + System.out.println("AT deployed successfully."); // Mint some blocks so that we are able to archive them later + System.out.println("Minting 1000 blocks..."); for (int i = 0; i < 1000; i++) { BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share")); + // Log every 100 blocks + if ((i + 1) % 100 == 0) { + System.out.println("Minted block " + (i + 1)); + } } + System.out.println("Finished minting blocks."); // Make sure that block 500 has full AT state data and data hash + System.out.println("Verifying block 500 AT state data..."); List block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); ATStateData atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); assertNotNull(atStatesData.getStateHash()); assertNotNull(atStatesData.getStateData()); + System.out.println("Block 500 AT state data verified."); // Trim the first 500 blocks + System.out.println("Trimming first 500 blocks..."); repository.getBlockRepository().trimOldOnlineAccountsSignatures(0, 500); repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(501); repository.getATRepository().rebuildLatestAtStates(500); repository.getATRepository().trimAtStates(0, 500, 1000); repository.getATRepository().setAtTrimHeight(501); + System.out.println("Trimming completed."); // Now block 499 should only have the AT state data hash + System.out.println("Checking block 499 AT state data..."); List block499AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(499); atStatesData = repository.getATRepository().getATStateAtHeight(block499AtStatesData.get(0).getATAddress(), 499); assertNotNull(atStatesData.getStateHash()); assertNull(atStatesData.getStateData()); + System.out.println("Block 499 AT state data contains only state hash as expected."); // ... but block 500 should have the full data (due to being retained as the "latest" AT state in the trimmed range + System.out.println("Verifying block 500 AT state data again..."); block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500); atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500); assertNotNull(atStatesData.getStateHash()); assertNotNull(atStatesData.getStateData()); + System.out.println("Block 500 AT state data contains full data."); // ... and block 501 should also have the full data + System.out.println("Verifying block 501 AT state data..."); List block501AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(501); atStatesData = repository.getATRepository().getATStateAtHeight(block501AtStatesData.get(0).getATAddress(), 501); assertNotNull(atStatesData.getStateHash()); assertNotNull(atStatesData.getStateData()); + System.out.println("Block 501 AT state data contains full data."); // Check the max archive height - this should be one less than the first untrimmed height final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository); + System.out.println("Maximum archive height determined (Expected 500): " + maximumArchiveHeight); assertEquals(500, maximumArchiveHeight); BlockData block3DataPreArchive = repository.getBlockRepository().fromHeight(3); // Write blocks 2-500 to the archive + System.out.println("Writing blocks 2 to " + maximumArchiveHeight + " to the archive..."); BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository); writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes BlockArchiveWriter.BlockArchiveWriteResult result = writer.write(); + System.out.println("Finished writing blocks to archive. Result: " + result); assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result); // Make sure that the archive contains the correct number of blocks + System.out.println("Number of blocks written to archive (Expected 499): " + writer.getWrittenCount()); assertEquals(500 - 1, writer.getWrittenCount()); // -1 for the genesis block // Increment block archive height repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount()); repository.saveChanges(); assertEquals(500 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight()); + System.out.println("Block archive height updated to: " + (500 - 1)); // Ensure the file exists File outputFile = writer.getOutputPath().toFile(); assertTrue(outputFile.exists()); + System.out.println("Archive file exists at: " + outputFile.getAbsolutePath()); // Ensure the SQL repository contains blocks 2 and 500... + System.out.println("Verifying that blocks 2 and 500 exist in the repository..."); assertNotNull(repository.getBlockRepository().fromHeight(2)); assertNotNull(repository.getBlockRepository().fromHeight(500)); + System.out.println("Blocks 2 and 500 are present in the repository."); // Prune all the archived blocks + System.out.println("Pruning blocks 2 to 500..."); int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 500); + System.out.println("Number of blocks pruned (Expected 499): " + numBlocksPruned); assertEquals(500-1, numBlocksPruned); repository.getBlockRepository().setBlockPruneHeight(501); // Prune the AT states for the archived blocks + System.out.println("Pruning AT states up to height 500..."); repository.getATRepository().rebuildLatestAtStates(500); repository.saveChanges(); int numATStatesPruned = repository.getATRepository().pruneAtStates(2, 500); + System.out.println("Number of AT states pruned (Expected 498): " + numATStatesPruned); assertEquals(498, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state repository.getATRepository().setAtPruneHeight(501); // Now ensure the SQL repository is missing blocks 2 and 500... + System.out.println("Verifying that blocks 2 and 500 have been pruned..."); assertNull(repository.getBlockRepository().fromHeight(2)); assertNull(repository.getBlockRepository().fromHeight(500)); + System.out.println("Blocks 2 and 500 have been successfully pruned."); // ... but it's not missing blocks 1 and 501 (we don't prune the genesis block) + System.out.println("Verifying that blocks 1 and 501 still exist..."); assertNotNull(repository.getBlockRepository().fromHeight(1)); assertNotNull(repository.getBlockRepository().fromHeight(501)); + System.out.println("Blocks 1 and 501 are present in the repository."); // Validate the latest block height in the repository - assertEquals(1002, (int) repository.getBlockRepository().getLastBlock().getHeight()); + int lastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("Latest block height in repository (Expected 1002): " + lastBlockHeight); + assertEquals(1002, lastBlockHeight); // Now orphan some unarchived blocks. + System.out.println("Orphaning 500 blocks..."); BlockUtils.orphanBlocks(repository, 500); - assertEquals(502, (int) repository.getBlockRepository().getLastBlock().getHeight()); + int currentLastBlockHeight = repository.getBlockRepository().getLastBlock().getHeight(); + System.out.println("New last block height after orphaning (Expected 502): " + currentLastBlockHeight); + assertEquals(502, currentLastBlockHeight); // We're close to the lower limit of the SQL database now, so // we need to import some blocks from the archive + System.out.println("Importing blocks 401 to 500 from the archive..."); BlockArchiveUtils.importFromArchive(401, 500, repository); // Ensure the SQL repository now contains block 401 but not 400... + System.out.println("Verifying that block 401 exists and block 400 does not..."); assertNotNull(repository.getBlockRepository().fromHeight(401)); assertNull(repository.getBlockRepository().fromHeight(400)); + System.out.println("Block 401 exists, block 400 does not."); // Import the remaining 399 blocks + System.out.println("Importing blocks 2 to 400 from the archive..."); BlockArchiveUtils.importFromArchive(2, 400, repository); // Verify that block 3 matches the original + System.out.println("Verifying that block 3 matches the original data..."); BlockData block3DataPostArchive = repository.getBlockRepository().fromHeight(3); assertArrayEquals(block3DataPreArchive.getSignature(), block3DataPostArchive.getSignature()); assertEquals(block3DataPreArchive.getHeight(), block3DataPostArchive.getHeight()); + System.out.println("Block 3 data matches the original."); // Orphan 2 more block, which should be the last one that is possible to be orphaned // TODO: figure out why this is 1 block more than in the equivalent block archive V1 test + System.out.println("Orphaning 2 more blocks..."); BlockUtils.orphanBlocks(repository, 2); + System.out.println("Orphaned 2 blocks successfully."); // Orphan another block, which should fail + System.out.println("Attempting to orphan another block, which should fail..."); Exception exception = null; try { BlockUtils.orphanBlocks(repository, 1); } catch (DataException e) { exception = e; + System.out.println("Caught expected DataException: " + e.getMessage()); } // Ensure that a DataException is thrown because there is no more AT states data available assertNotNull(exception); assertEquals(DataException.class, exception.getClass()); + System.out.println("DataException confirmed due to lack of AT states data."); // FUTURE: we may be able to retain unique AT states when trimming, to avoid this exception // and allow orphaning back through blocks with trimmed AT states. + System.out.println("testTrimArchivePruneAndOrphan completed successfully."); } } @@ -477,16 +619,26 @@ public class BlockArchiveV2Tests extends Common { public void testMissingAtStatesHeightIndex() throws DataException, SQLException { try (final HSQLDBRepository repository = (HSQLDBRepository) RepositoryManager.getRepository()) { + System.out.println("Starting testMissingAtStatesHeightIndex"); + // Firstly check that we're able to prune or archive when the index exists + System.out.println("Checking existence of ATStatesHeightIndex..."); assertTrue(repository.getATRepository().hasAtStatesHeightIndex()); assertTrue(RepositoryManager.canArchiveOrPrune()); + System.out.println("ATStatesHeightIndex exists. Archiving and pruning are possible."); // Delete the index + System.out.println("Dropping ATStatesHeightIndex..."); repository.prepareStatement("DROP INDEX ATSTATESHEIGHTINDEX").execute(); + System.out.println("ATStatesHeightIndex dropped."); // Ensure check that we're unable to prune or archive when the index doesn't exist + System.out.println("Verifying that ATStatesHeightIndex no longer exists..."); assertFalse(repository.getATRepository().hasAtStatesHeightIndex()); assertFalse(RepositoryManager.canArchiveOrPrune()); + System.out.println("ATStatesHeightIndex does not exist. Archiving and pruning are disabled."); + + System.out.println("testMissingAtStatesHeightIndex completed successfully."); } } @@ -496,8 +648,10 @@ public class BlockArchiveV2Tests extends Common { Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toAbsolutePath(); try { FileUtils.deleteDirectory(archivePath.toFile()); + System.out.println("Deleted archive directory at: " + archivePath); } catch (IOException e) { + System.out.println("Failed to delete archive directory: " + e.getMessage()); } } diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json index 17fc80c4..4e49e86d 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -81,7 +81,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 9999999999999, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -91,7 +91,14 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json index 33054732..9ad59d79 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -84,7 +84,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 0, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -94,7 +94,14 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index 577a07f1..e4182d7d 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -95,7 +95,14 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index 82e4ace7..04005b2b 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -95,7 +95,14 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index 16032a9c..ddb29ca5 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 0, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -95,7 +95,14 @@ "arbitraryOptionalFeeTimestamp": 9999999999999, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-penalty-fix.json b/src/test/resources/test-chain-v2-penalty-fix.json index e62fc9f2..cac92c16 100644 --- a/src/test/resources/test-chain-v2-penalty-fix.json +++ b/src/test/resources/test-chain-v2-penalty-fix.json @@ -83,16 +83,24 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, - "selfSponsorshipAlgoV1Height": 99999999, + "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, - "selfSponsorshipAlgoV2Height": 9999999, "disableTransferPrivsTimestamp": 9999999999500, "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999, "penaltyFixHeight": 5 }, "genesisInfo": { diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json index 3ec11942..566d8515 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -95,7 +95,14 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder-reduction.json b/src/test/resources/test-chain-v2-qora-holder-reduction.json index 2b8834ce..c7ed2270 100644 --- a/src/test/resources/test-chain-v2-qora-holder-reduction.json +++ b/src/test/resources/test-chain-v2-qora-holder-reduction.json @@ -86,7 +86,7 @@ "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, "aggregateSignatureTimestamp": 0, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -96,7 +96,14 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index ab96a243..1c4f0d93 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -95,7 +95,14 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index 35535c75..30d952e1 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -95,7 +95,14 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index 616d0925..612f02a5 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -95,7 +95,14 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 500, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json index ec6ffd2e..2f332233 100644 --- a/src/test/resources/test-chain-v2-reward-shares.json +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -95,7 +95,14 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json b/src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json index d0d989cf..3ea8bc70 100644 --- a/src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 20, "selfSponsorshipAlgoV2Height": 999999999, @@ -95,7 +95,14 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json b/src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json index 5f09cb47..ae424704 100644 --- a/src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 30, @@ -95,7 +95,14 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json b/src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json index f7d1faa2..2a24473b 100644 --- a/src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json @@ -85,7 +85,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -95,7 +95,14 @@ "arbitraryOptionalFeeTimestamp": 0, "unconfirmableRewardSharesHeight": 99999999, "disableTransferPrivsTimestamp": 9999999999500, - "enableTransferPrivsTimestamp": 9999999999950 + "enableTransferPrivsTimestamp": 9999999999950, + "cancelSellNameValidationTimestamp": 9999999999999, + "disableRewardshareHeight": 9999999999990, + "enableRewardshareHeight": 9999999999999, + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 086c126e..c829975b 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -86,7 +86,7 @@ "transactionV5Timestamp": 0, "transactionV6Timestamp": 0, "disableReferenceTimestamp": 9999999999999, - "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999990, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, "selfSponsorshipAlgoV2Height": 999999999, @@ -100,8 +100,10 @@ "cancelSellNameValidationTimestamp": 9999999999999, "disableRewardshareHeight": 9999999999990, "enableRewardshareHeight": 9999999999999, - "onlyMintWithNameHeight": 9999999999999, - "groupMemberCheckHeight": 9999999999999 + "onlyMintWithNameHeight": 9999999999990, + "groupMemberCheckHeight": 9999999999999, + "decreaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "removeOnlyMintWithNameHeight": 9999999999999 }, "genesisInfo": { "version": 4, From 448b536238c12e03a70abfee5339e9b308e42b5e Mon Sep 17 00:00:00 2001 From: crowetic Date: Tue, 3 Dec 2024 09:09:42 -0800 Subject: [PATCH 10/16] Modified start script to work with optimized Garbage Collection made available in version 4.6.6 and beyond. Suggestion to machines with 6GB of RAM or less, increase the percentage from 50 to 75. Qortal Core will only utilize the RAM it needs, up to the percentage set maximum. --- start.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/start.sh b/start.sh index cc80dceb..88937026 100755 --- a/start.sh +++ b/start.sh @@ -33,8 +33,13 @@ fi # Limits Java JVM stack size and maximum heap usage. # Comment out for bigger systems, e.g. non-routers # or when API documentation is enabled -# Uncomment (remove '#' sign) line below if your system has less than 12GB of RAM for optimal RAM defaults -JVM_MEMORY_ARGS="-Xss256m -XX:+UseSerialGC" +# JAVA MEMORY SETTINGS BELOW - These settings are essentially optimized default settings. +# Combined with the latest changes on the Qortal Core in version 4.6.6 and beyond, +# should give a dramatic increase In performance due to optimized Garbage Collection. +# These memory arguments should work on machines with as little as 6GB of RAM. +# If you want to run on a machine with less than 6GB of RAM, it is suggested to increase the '50' below to '75' +# The Qortal Core will utilize only as much RAM as it needs, but up-to the amount set in percentage below. +JVM_MEMORY_ARGS="-XX:MaxRAMPercentage=50 -XX:+UseG1GC -Xss1024k" # Although java.net.preferIPv4Stack is supposed to be false # by default in Java 11, on some platforms (e.g. FreeBSD 12), From 8d6830135c4bd98f251fe761a8da94b540053960 Mon Sep 17 00:00:00 2001 From: crowetic Date: Wed, 4 Dec 2024 14:07:43 -0800 Subject: [PATCH 11/16] Changes need to be reverted prior to new PR from crowetic repo. Revert "Update dependencies" This reverts commit e3a85786e7ebd7fa78f8bf711ee5493e136fc149. --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 11c06bc0..bde846a0 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ 1.2.2 0.12.3 4.9.10 - 1.68.2 + 1.68.1 33.3.1-jre 2.2 1.2.1 @@ -35,7 +35,7 @@ 9.4.56.v20240826 1.1.1 20240303 - 1.18.3 + 1.18.1 5.11.0-M2 1.0.0 2.23.1 From 9b20192b3057eb0b147a2240a923504eb6068fa8 Mon Sep 17 00:00:00 2001 From: crowetic Date: Wed, 4 Dec 2024 14:08:25 -0800 Subject: [PATCH 12/16] Changes need to be reverted prior to the PR from crowetic repo being merged. All of these changes aside from those in the 'network' folder, will be re-applied with crowetic's PR. Revert "Various changes" This reverts commit adbba0f94767cda6251668c5206015dfccb44941. --- src/main/java/org/qortal/account/Account.java | 20 +--- .../qortal/api/model/ApiOnlineAccount.java | 33 ------- .../qortal/api/model/BlockMintingInfo.java | 2 +- .../qortal/api/resource/BlocksResource.java | 3 +- src/main/java/org/qortal/block/Block.java | 51 +++------- .../java/org/qortal/data/block/BlockData.java | 29 +----- .../java/org/qortal/network/Handshake.java | 26 ++--- src/main/java/org/qortal/network/Network.java | 98 ++++++++++--------- 8 files changed, 82 insertions(+), 180 deletions(-) diff --git a/src/main/java/org/qortal/account/Account.java b/src/main/java/org/qortal/account/Account.java index 99fa5217..537f0788 100644 --- a/src/main/java/org/qortal/account/Account.java +++ b/src/main/java/org/qortal/account/Account.java @@ -348,27 +348,9 @@ public class Account { return accountData.getLevel(); } - /** - * Returns reward-share minting address, or unknown if reward-share does not exist. - * - * @param repository - * @param rewardSharePublicKey - * @return address or unknown - * @throws DataException - */ - public static String getRewardShareMintingAddress(Repository repository, byte[] rewardSharePublicKey) throws DataException { - // Find actual minter address - RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(rewardSharePublicKey); - - if (rewardShareData == null) - return "Unknown"; - - return rewardShareData.getMinter(); - } - /** * Returns 'effective' minting level, or zero if reward-share does not exist. - * + * * @param repository * @param rewardSharePublicKey * @return 0+ diff --git a/src/main/java/org/qortal/api/model/ApiOnlineAccount.java b/src/main/java/org/qortal/api/model/ApiOnlineAccount.java index e26eb816..08b697aa 100644 --- a/src/main/java/org/qortal/api/model/ApiOnlineAccount.java +++ b/src/main/java/org/qortal/api/model/ApiOnlineAccount.java @@ -1,13 +1,7 @@ package org.qortal.api.model; -import org.qortal.account.Account; -import org.qortal.repository.DataException; -import org.qortal.repository.RepositoryManager; -import org.qortal.repository.Repository; - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; // All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) @@ -53,31 +47,4 @@ public class ApiOnlineAccount { return this.recipientAddress; } - public int getMinterLevelFromPublicKey() { - try (final Repository repository = RepositoryManager.getRepository()) { - return Account.getRewardShareEffectiveMintingLevel(repository, this.rewardSharePublicKey); - } catch (DataException e) { - return 0; - } - } - - public boolean getIsMember() { - try (final Repository repository = RepositoryManager.getRepository()) { - return repository.getGroupRepository().memberExists(694, getMinterAddress()); - } catch (DataException e) { - return false; - } - } - - // JAXB special - - @XmlElement(name = "minterLevel") - protected int getMinterLevel() { - return getMinterLevelFromPublicKey(); - } - - @XmlElement(name = "isMinterMember") - protected boolean getMinterMember() { - return getIsMember(); - } } diff --git a/src/main/java/org/qortal/api/model/BlockMintingInfo.java b/src/main/java/org/qortal/api/model/BlockMintingInfo.java index 02765a89..f84e179e 100644 --- a/src/main/java/org/qortal/api/model/BlockMintingInfo.java +++ b/src/main/java/org/qortal/api/model/BlockMintingInfo.java @@ -9,7 +9,6 @@ import java.math.BigInteger; public class BlockMintingInfo { public byte[] minterPublicKey; - public String minterAddress; public int minterLevel; public int onlineAccountsCount; public BigDecimal maxDistance; @@ -20,4 +19,5 @@ public class BlockMintingInfo { public BlockMintingInfo() { } + } diff --git a/src/main/java/org/qortal/api/resource/BlocksResource.java b/src/main/java/org/qortal/api/resource/BlocksResource.java index ff0bb979..01d8d2ab 100644 --- a/src/main/java/org/qortal/api/resource/BlocksResource.java +++ b/src/main/java/org/qortal/api/resource/BlocksResource.java @@ -542,7 +542,6 @@ public class BlocksResource { } } - String minterAddress = Account.getRewardShareMintingAddress(repository, blockData.getMinterPublicKey()); int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey()); if (minterLevel == 0) // This may be unavailable when requesting a trimmed block @@ -555,7 +554,6 @@ public class BlocksResource { BlockMintingInfo blockMintingInfo = new BlockMintingInfo(); blockMintingInfo.minterPublicKey = blockData.getMinterPublicKey(); - blockMintingInfo.minterAddress = minterAddress; blockMintingInfo.minterLevel = minterLevel; blockMintingInfo.onlineAccountsCount = blockData.getOnlineAccountsCount(); blockMintingInfo.maxDistance = new BigDecimal(block.MAX_DISTANCE); @@ -889,4 +887,5 @@ public class BlocksResource { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); } } + } diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index c9353d70..918a20ae 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -145,7 +145,7 @@ public class Block { private final Account recipientAccount; private final AccountData recipientAccountData; - + final BlockChain blockChain = BlockChain.getInstance(); ExpandedAccount(Repository repository, RewardShareData rewardShareData) throws DataException { @@ -414,21 +414,6 @@ public class Block { }); } - // After feature trigger, remove any online accounts that are not minter group member - if (height >= BlockChain.getInstance().getGroupMemberCheckHeight()) { - onlineAccounts.removeIf(a -> { - try { - int groupId = BlockChain.getInstance().getMintingGroupId(); - String address = Account.getRewardShareMintingAddress(repository, a.getPublicKey()); - boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address); - return !isMinterGroupMember; - } catch (DataException e) { - // Something went wrong, so remove the account - return true; - } - }); - } - if (onlineAccounts.isEmpty()) { LOGGER.debug("No online accounts - not even our own?"); return null; @@ -736,19 +721,19 @@ public class Block { List expandedAccounts = new ArrayList<>(); for (RewardShareData rewardShare : this.cachedOnlineRewardShares) { - int groupId = BlockChain.getInstance().getMintingGroupId(); - String address = rewardShare.getMinter(); - boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address); - - if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) - expandedAccounts.add(new ExpandedAccount(repository, rewardShare)); - - if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight() && isMinterGroupMember) + if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) { expandedAccounts.add(new ExpandedAccount(repository, rewardShare)); + } + if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight()) { + boolean isMinterGroupMember = repository.getGroupRepository().memberExists(BlockChain.getInstance().getMintingGroupId(), rewardShare.getMinter()); + if (isMinterGroupMember) { + expandedAccounts.add(new ExpandedAccount(repository, rewardShare)); + } + } } + this.cachedExpandedAccounts = expandedAccounts; - LOGGER.trace(() -> String.format("Online reward-shares after expanded accounts %s", this.cachedOnlineRewardShares)); return this.cachedExpandedAccounts; } @@ -1158,17 +1143,8 @@ public class Block { if (this.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) { List expandedAccounts = this.getExpandedAccounts(); for (ExpandedAccount account : expandedAccounts) { - int groupId = BlockChain.getInstance().getMintingGroupId(); - String address = account.getMintingAccount().getAddress(); - boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address); - if (account.getMintingAccount().getEffectiveMintingLevel() == 0) return ValidationResult.ONLINE_ACCOUNTS_INVALID; - - if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight()) { - if (!isMinterGroupMember) - return ValidationResult.ONLINE_ACCOUNTS_INVALID; - } } } @@ -1297,7 +1273,6 @@ public class Block { // Online Accounts ValidationResult onlineAccountsResult = this.areOnlineAccountsValid(); - LOGGER.trace("Accounts valid = {}", onlineAccountsResult); if (onlineAccountsResult != ValidationResult.OK) return onlineAccountsResult; @@ -1386,7 +1361,7 @@ public class Block { // Check transaction can even be processed validationResult = transaction.isProcessable(); if (validationResult != Transaction.ValidationResult.OK) { - LOGGER.debug(String.format("Error during transaction validation, tx %s: %s", Base58.encode(transactionData.getSignature()), validationResult.name())); + LOGGER.info(String.format("Error during transaction validation, tx %s: %s", Base58.encode(transactionData.getSignature()), validationResult.name())); return ValidationResult.TRANSACTION_INVALID; } @@ -1587,7 +1562,6 @@ public class Block { this.blockData.setHeight(blockchainHeight + 1); LOGGER.trace(() -> String.format("Processing block %d", this.blockData.getHeight())); - LOGGER.trace(() -> String.format("Online Reward Shares in process %s", this.cachedOnlineRewardShares)); if (this.blockData.getHeight() > 1) { @@ -2306,6 +2280,7 @@ public class Block { // Select the correct set of share bins based on block height List accountLevelShareBinsForBlock = (this.blockData.getHeight() >= BlockChain.getInstance().getSharesByLevelV2Height()) ? BlockChain.getInstance().getAccountLevelShareBinsV2() : BlockChain.getInstance().getAccountLevelShareBinsV1(); + // Determine reward candidates based on account level // This needs a deep copy, so the shares can be modified when tiers aren't activated yet List accountLevelShareBins = new ArrayList<>(); @@ -2595,11 +2570,9 @@ public class Block { return; int minterLevel = Account.getRewardShareEffectiveMintingLevel(this.repository, this.getMinter().getPublicKey()); - String minterAddress = Account.getRewardShareMintingAddress(this.repository, this.getMinter().getPublicKey()); LOGGER.debug(String.format("======= BLOCK %d (%.8s) =======", this.getBlockData().getHeight(), Base58.encode(this.getSignature()))); LOGGER.debug(String.format("Timestamp: %d", this.getBlockData().getTimestamp())); - LOGGER.debug(String.format("Minter address: %s", minterAddress)); LOGGER.debug(String.format("Minter level: %d", minterLevel)); LOGGER.debug(String.format("Online accounts: %d", this.getBlockData().getOnlineAccountsCount())); LOGGER.debug(String.format("AT count: %d", this.getBlockData().getATCount())); diff --git a/src/main/java/org/qortal/data/block/BlockData.java b/src/main/java/org/qortal/data/block/BlockData.java index 7e2a1872..34df0f9a 100644 --- a/src/main/java/org/qortal/data/block/BlockData.java +++ b/src/main/java/org/qortal/data/block/BlockData.java @@ -1,11 +1,8 @@ package org.qortal.data.block; import com.google.common.primitives.Bytes; -import org.qortal.account.Account; import org.qortal.block.BlockChain; -import org.qortal.repository.DataException; -import org.qortal.repository.Repository; -import org.qortal.repository.RepositoryManager; +import org.qortal.crypto.Crypto; import org.qortal.settings.Settings; import org.qortal.utils.NTP; @@ -227,7 +224,7 @@ public class BlockData implements Serializable { } return 0; } - + public boolean isTrimmed() { long onlineAccountSignaturesTrimmedTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime(); long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime(); @@ -235,31 +232,11 @@ public class BlockData implements Serializable { return blockTimestamp < onlineAccountSignaturesTrimmedTimestamp && blockTimestamp < currentTrimmableTimestamp; } - public String getMinterAddressFromPublicKey() { - try (final Repository repository = RepositoryManager.getRepository()) { - return Account.getRewardShareMintingAddress(repository, this.minterPublicKey); - } catch (DataException e) { - return "Unknown"; - } - } - - public int getMinterLevelFromPublicKey() { - try (final Repository repository = RepositoryManager.getRepository()) { - return Account.getRewardShareEffectiveMintingLevel(repository, this.minterPublicKey); - } catch (DataException e) { - return 0; - } - } - // JAXB special @XmlElement(name = "minterAddress") protected String getMinterAddress() { - return getMinterAddressFromPublicKey(); + return Crypto.toAddress(this.minterPublicKey); } - @XmlElement(name = "minterLevel") - protected int getMinterLevel() { - return getMinterLevelFromPublicKey(); - } } diff --git a/src/main/java/org/qortal/network/Handshake.java b/src/main/java/org/qortal/network/Handshake.java index 07f14702..081e79e6 100644 --- a/src/main/java/org/qortal/network/Handshake.java +++ b/src/main/java/org/qortal/network/Handshake.java @@ -48,7 +48,7 @@ public enum Handshake { String versionString = helloMessage.getVersionString(); - Matcher matcher = Peer.VERSION_PATTERN.matcher(versionString); + Matcher matcher = peer.VERSION_PATTERN.matcher(versionString); if (!matcher.lookingAt()) { LOGGER.debug(() -> String.format("Peer %s sent invalid HELLO version string '%s'", peer, versionString)); return null; @@ -71,7 +71,7 @@ public enum Handshake { // Ensure the peer is running at least the version specified in MIN_PEER_VERSION if (!peer.isAtLeastVersion(MIN_PEER_VERSION)) { - LOGGER.debug("Ignoring peer {} because it is on an old version ({})", peer, versionString); + LOGGER.debug(String.format("Ignoring peer %s because it is on an old version (%s)", peer, versionString)); return null; } @@ -79,7 +79,7 @@ public enum Handshake { // Ensure the peer is running at least the minimum version allowed for connections final String minPeerVersion = Settings.getInstance().getMinPeerVersion(); if (!peer.isAtLeastVersion(minPeerVersion)) { - LOGGER.debug("Ignoring peer {} because it is on an old version ({})", peer, versionString); + LOGGER.debug(String.format("Ignoring peer %s because it is on an old version (%s)", peer, versionString)); return null; } } @@ -106,7 +106,7 @@ public enum Handshake { byte[] peersPublicKey = challengeMessage.getPublicKey(); byte[] peersChallenge = challengeMessage.getChallenge(); - // If public key matches our public key, then we've connected to self + // If public key matches our public key then we've connected to self byte[] ourPublicKey = Network.getInstance().getOurPublicKey(); if (Arrays.equals(ourPublicKey, peersPublicKey)) { // If outgoing connection then record destination as self so we don't try again @@ -121,11 +121,11 @@ public enum Handshake { peer.disconnect("failed to send CHALLENGE to self"); /* - * We return the CHALLENGE here to prevent us from closing the connection. - * Closing the connection currently preempts the remote end from reading any pending messages, + * We return CHALLENGE here to prevent us from closing connection. Closing + * connection currently preempts remote end from reading any pending messages, * specifically the CHALLENGE message we just sent above. When our 'remote' * outbound counterpart reads our message, they will close both connections. - * Failing that, our connection will time out or a future handshake error will + * Failing that, our connection will timeout or a future handshake error will * occur. */ return CHALLENGE; @@ -135,7 +135,7 @@ public enum Handshake { // Are we already connected to this peer? Peer existingPeer = Network.getInstance().getHandshakedPeerWithPublicKey(peersPublicKey); if (existingPeer != null) { - LOGGER.debug(() -> String.format("We already have a connection with peer %s - discarding", peer)); + LOGGER.info(() -> String.format("We already have a connection with peer %s - discarding", peer)); // Handshake failure - caller will deal with disconnect return null; } @@ -148,7 +148,7 @@ public enum Handshake { @Override public void action(Peer peer) { - // Send a challenge + // Send challenge byte[] publicKey = Network.getInstance().getOurPublicKey(); byte[] challenge = peer.getOurChallenge(); @@ -254,17 +254,16 @@ public enum Handshake { private static final Logger LOGGER = LogManager.getLogger(Handshake.class); - /** The Maximum allowed difference between peer's reported timestamp and when they connected, in milliseconds. */ + /** Maximum allowed difference between peer's reported timestamp and when they connected, in milliseconds. */ private static final long MAX_TIMESTAMP_DELTA = 30 * 1000L; // ms private static final long PEER_VERSION_131 = 0x0100030001L; /** Minimum peer version that we are allowed to communicate with */ - private static final String MIN_PEER_VERSION = "4.6.5"; + private static final String MIN_PEER_VERSION = "4.1.1"; private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits - // Can always be made harder in the future... private static final int POW_BUFFER_SIZE_POST_131 = 2 * 1024 * 1024; // bytes private static final int POW_DIFFICULTY_POST_131 = 2; // leading zero bits @@ -276,11 +275,12 @@ public enum Handshake { public final MessageType expectedMessageType; - Handshake(MessageType expectedMessageType) { + private Handshake(MessageType expectedMessageType) { this.expectedMessageType = expectedMessageType; } public abstract Handshake onMessage(Peer peer, Message message); public abstract void action(Peer peer); + } diff --git a/src/main/java/org/qortal/network/Network.java b/src/main/java/org/qortal/network/Network.java index d8777eec..f500b2e8 100644 --- a/src/main/java/org/qortal/network/Network.java +++ b/src/main/java/org/qortal/network/Network.java @@ -80,7 +80,7 @@ public class Network { "node.qortal.ru", "node2.qortal.ru", "node3.qortal.ru", "node.qortal.uk", "node22.qortal.org", "cinfu1.crowetic.com", "node.cwd.systems", "bootstrap.cwd.systems", "node1.qortalnodes.live", "node2.qortalnodes.live", "node3.qortalnodes.live", "node4.qortalnodes.live", "node5.qortalnodes.live", - "node.qortalnodes.live", "qortex.live", + "node6.qortalnodes.live", "node7.qortalnodes.live", "node8.qortalnodes.live" }; private static final long NETWORK_EPC_KEEPALIVE = 5L; // seconds @@ -149,7 +149,7 @@ public class Network { private final Lock mergePeersLock = new ReentrantLock(); - private final List ourExternalIpAddressHistory = new ArrayList<>(); + private List ourExternalIpAddressHistory = new ArrayList<>(); private String ourExternalIpAddress = null; private int ourExternalPort = Settings.getInstance().getListenPort(); @@ -167,7 +167,7 @@ public class Network { ExecutorService networkExecutor = new ThreadPoolExecutor(2, Settings.getInstance().getMaxNetworkThreadPoolSize(), NETWORK_EPC_KEEPALIVE, TimeUnit.SECONDS, - new SynchronousQueue<>(), + new SynchronousQueue(), new NamedThreadFactory("Network-EPC", Settings.getInstance().getNetworkThreadPriority())); networkEPC = new NetworkProcessor(networkExecutor); } @@ -314,7 +314,7 @@ public class Network { public List getImmutableConnectedDataPeers() { return this.getImmutableConnectedPeers().stream() - .filter(Peer::isDataPeer) + .filter(p -> p.isDataPeer()) .collect(Collectors.toList()); } @@ -346,7 +346,7 @@ public class Network { public boolean requestDataFromPeer(String peerAddressString, byte[] signature) { if (peerAddressString != null) { PeerAddress peerAddress = PeerAddress.fromString(peerAddressString); - PeerData peerData; + PeerData peerData = null; // Reuse an existing PeerData instance if it's already in the known peers list synchronized (this.allKnownPeers) { @@ -370,9 +370,9 @@ public class Network { // Check if we're already connected to and handshaked with this peer Peer connectedPeer = this.getImmutableConnectedPeers().stream() - .filter(p -> p.getPeerData().getAddress().equals(peerAddress)) - .findFirst() - .orElse(null); + .filter(p -> p.getPeerData().getAddress().equals(peerAddress)) + .findFirst() + .orElse(null); boolean isConnected = (connectedPeer != null); @@ -710,7 +710,7 @@ public class Network { return true; } - private Peer getConnectablePeer(final Long now) { + private Peer getConnectablePeer(final Long now) throws InterruptedException { // We can't block here so use tryRepository(). We don't NEED to connect a new peer. try (Repository repository = RepositoryManager.tryRepository()) { if (repository == null) { @@ -807,7 +807,7 @@ public class Network { // Find peers that have reached their maximum connection age, and disconnect them List peersToDisconnect = this.getImmutableConnectedPeers().stream() .filter(peer -> !peer.isSyncInProgress()) - .filter(Peer::hasReachedMaxConnectionAge) + .filter(peer -> peer.hasReachedMaxConnectionAge()) .collect(Collectors.toList()); if (peersToDisconnect != null && !peersToDisconnect.isEmpty()) { @@ -996,9 +996,9 @@ public class Network { } // Add to per-message thread count (first initializing to 0 if not already present) - threadsPerMessageType.putIfAbsent(message.getType(), 0); + threadsPerMessageType.computeIfAbsent(message.getType(), key -> 0); threadsPerMessageType.computeIfPresent(message.getType(), (key, value) -> value + 1); - + // Add to total thread count synchronized (this) { totalThreadCount++; @@ -1037,7 +1037,7 @@ public class Network { } // Remove from per-message thread count (first initializing to 0 if not already present) - threadsPerMessageType.putIfAbsent(message.getType(), 0); + threadsPerMessageType.computeIfAbsent(message.getType(), key -> 0); threadsPerMessageType.computeIfPresent(message.getType(), (key, value) -> value - 1); // Remove from total thread count @@ -1135,7 +1135,7 @@ public class Network { Peer existingPeer = getHandshakedPeerWithPublicKey(peer.getPeersPublicKey()); // NOTE: actual object reference compare, not Peer.equals() if (existingPeer != peer) { - LOGGER.debug("[{}] We already have a connection with peer {} - discarding", + LOGGER.info("[{}] We already have a connection with peer {} - discarding", peer.getPeerConnectionId(), peer); peer.disconnect("existing connection"); return; @@ -1216,7 +1216,29 @@ public class Network { * Returns PEERS message made from peers we've connected to recently, and this node's details */ public Message buildPeersMessage(Peer peer) { - final var knownPeers = getPeerData(); + List knownPeers = this.getAllKnownPeers(); + + // Filter out peers that we've not connected to ever or within X milliseconds + final long connectionThreshold = NTP.getTime() - RECENT_CONNECTION_THRESHOLD; + Predicate notRecentlyConnected = peerData -> { + final Long lastAttempted = peerData.getLastAttempted(); + final Long lastConnected = peerData.getLastConnected(); + + if (lastAttempted == null || lastConnected == null) { + return true; + } + + if (lastConnected < lastAttempted) { + return true; + } + + if (lastConnected < connectionThreshold) { + return true; + } + + return false; + }; + knownPeers.removeIf(notRecentlyConnected); List peerAddresses = new ArrayList<>(); @@ -1240,29 +1262,6 @@ public class Network { return new PeersV2Message(peerAddresses); } - private List getPeerData() { - List knownPeers = this.getAllKnownPeers(); - - // Filter out peers that we've not connected to ever or within X milliseconds - final long connectionThreshold = NTP.getTime() - RECENT_CONNECTION_THRESHOLD; - Predicate notRecentlyConnected = peerData -> { - final Long lastAttempted = peerData.getLastAttempted(); - final Long lastConnected = peerData.getLastConnected(); - - if (lastAttempted == null || lastConnected == null) { - return true; - } - - if (lastConnected < lastAttempted) { - return true; - } - - return lastConnected < connectionThreshold; - }; - knownPeers.removeIf(notRecentlyConnected); - return knownPeers; - } - /** Builds either (legacy) HeightV2Message or (newer) BlockSummariesV2Message, depending on peer version. * * @return Message, or null if DataException was thrown. @@ -1329,7 +1328,7 @@ public class Network { return; } String host = parts[0]; - + try { InetAddress addr = InetAddress.getByName(host); if (addr.isAnyLocalAddress() || addr.isSiteLocalAddress()) { @@ -1370,12 +1369,12 @@ public class Network { for (int i = size-1; i >= 0; i--) { String reading = ipAddressHistory.get(i); if (lastReading != null) { - if (Objects.equals(reading, lastReading)) { + if (Objects.equals(reading, lastReading)) { consecutiveReadings++; - } - else { - consecutiveReadings = 0; - } + } + else { + consecutiveReadings = 0; + } } lastReading = reading; } @@ -1516,8 +1515,12 @@ public class Network { return true; } - return peerData.getLastConnected() == null - || peerData.getLastConnected() > now - OLD_PEER_CONNECTION_PERIOD; + if (peerData.getLastConnected() == null + || peerData.getLastConnected() > now - OLD_PEER_CONNECTION_PERIOD) { + return true; + } + + return false; }; // Disregard peers that are NOT 'old' @@ -1652,7 +1655,7 @@ public class Network { // Stop processing threads try { - if (!this.networkEPC.shutdown(10000)) { + if (!this.networkEPC.shutdown(5000)) { LOGGER.warn("Network threads failed to terminate"); } } catch (InterruptedException e) { @@ -1664,4 +1667,5 @@ public class Network { peer.shutdown(); } } + } From 04203e7c31c943d48c96097615d32bc67e318d47 Mon Sep 17 00:00:00 2001 From: crowetic Date: Thu, 5 Dec 2024 20:00:31 -0800 Subject: [PATCH 13/16] modified autoUpdateRepos further to plan ahead. --- pom.xml | 2 +- .../java/org/qortal/settings/Settings.java | 85 ++++++++++--------- 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/pom.xml b/pom.xml index bde846a0..c6f8bd7a 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.qortal qortal - 4.6.5 + 4.6.6 jar UTF-8 diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index c3305e82..0e8e95f0 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -67,7 +67,7 @@ public class Settings { private Integer apiPort; private boolean apiWhitelistEnabled = true; private String[] apiWhitelist = new String[] { - "::1", "127.0.0.1" + "::1", "127.0.0.1" }; /** Legacy API key (deprecated Nov 2021). Use /admin/apikey/generate API endpoint instead */ @@ -213,7 +213,7 @@ public class Settings { public long recoveryModeTimeout = 9999999999999L; /** Minimum peer version number required in order to sync with them */ - private String minPeerVersion = "4.6.3"; + private String minPeerVersion = "4.6.5"; /** Whether to allow connections with peers below minPeerVersion * If true, we won't sync with them but they can still sync with us, and will show in the peers list * If false, sync will be blocked both ways, and they will not appear in the peers list */ @@ -222,7 +222,7 @@ public class Settings { /** Minimum time (in seconds) that we should attempt to remain connected to a peer for */ private int minPeerConnectionTime = 2 * 60 * 60; // seconds /** Maximum time (in seconds) that we should attempt to remain connected to a peer for */ - private int maxPeerConnectionTime = 4 * 60 * 60; // seconds + private int maxPeerConnectionTime = 6 * 60 * 60; // seconds /** Maximum time (in seconds) that a peer should remain connected when requesting QDN data */ private int maxDataPeerConnectionTime = 30 * 60; // seconds @@ -272,16 +272,19 @@ public class Settings { // Bootstrap sources private String[] bootstrapHosts = new String[] { - "http://bootstrap.qortal.org", - "http://bootstrap2.qortal.org", - "http://bootstrap3.qortal.org", - "http://bootstrap4.qortal.org" + "http://bootstrap.qortal.org", + "http://bootstrap2.qortal.org", + "http://bootstrap3.qortal.org", + "http://bootstrap4.qortal.org" }; // Auto-update sources private String[] autoUpdateRepos = new String[] { - "https://github.com/Qortal/qortal/raw/%s/qortal.update", - "https://raw.githubusercontent.com@151.101.16.133/Qortal/qortal/%s/qortal.update" + "https://github.com/Qortal/qortal/raw/%s/qortal.update", + "https://raw.githubusercontent.com@151.101.16.133/Qortal/qortal/%s/qortal.update", + "https://qortal.link/Auto-Update/%s/qortal.update", + "https://qortal.name/Auto-Update/%s/qortal.update", + "https://update.qortal.org/Auto-Update/%s/qortal.update" }; // Lists @@ -289,36 +292,36 @@ public class Settings { /** Array of NTP server hostnames. */ private String[] ntpServers = new String[] { - "pool.ntp.org", - "0.pool.ntp.org", - "1.pool.ntp.org", - "2.pool.ntp.org", - "3.pool.ntp.org", - "asia.pool.ntp.org", - "0.asia.pool.ntp.org", - "1.asia.pool.ntp.org", - "2.asia.pool.ntp.org", - "3.asia.pool.ntp.org", - "europe.pool.ntp.org", - "0.europe.pool.ntp.org", - "1.europe.pool.ntp.org", - "2.europe.pool.ntp.org", - "3.europe.pool.ntp.org", - "north-america.pool.ntp.org", - "0.north-america.pool.ntp.org", - "1.north-america.pool.ntp.org", - "2.north-america.pool.ntp.org", - "3.north-america.pool.ntp.org", - "oceania.pool.ntp.org", - "0.oceania.pool.ntp.org", - "1.oceania.pool.ntp.org", - "2.oceania.pool.ntp.org", - "3.oceania.pool.ntp.org", - "south-america.pool.ntp.org", - "0.south-america.pool.ntp.org", - "1.south-america.pool.ntp.org", - "2.south-america.pool.ntp.org", - "3.south-america.pool.ntp.org" + "pool.ntp.org", + "0.pool.ntp.org", + "1.pool.ntp.org", + "2.pool.ntp.org", + "3.pool.ntp.org", + "asia.pool.ntp.org", + "0.asia.pool.ntp.org", + "1.asia.pool.ntp.org", + "2.asia.pool.ntp.org", + "3.asia.pool.ntp.org", + "europe.pool.ntp.org", + "0.europe.pool.ntp.org", + "1.europe.pool.ntp.org", + "2.europe.pool.ntp.org", + "3.europe.pool.ntp.org", + "north-america.pool.ntp.org", + "0.north-america.pool.ntp.org", + "1.north-america.pool.ntp.org", + "2.north-america.pool.ntp.org", + "3.north-america.pool.ntp.org", + "oceania.pool.ntp.org", + "0.oceania.pool.ntp.org", + "1.oceania.pool.ntp.org", + "2.oceania.pool.ntp.org", + "3.oceania.pool.ntp.org", + "south-america.pool.ntp.org", + "0.south-america.pool.ntp.org", + "1.south-america.pool.ntp.org", + "2.south-america.pool.ntp.org", + "3.south-america.pool.ntp.org" }; /** Additional offset added to values returned by NTP.getTime() */ private Long testNtpOffset = null; @@ -408,7 +411,7 @@ public class Settings { * The thread priority (1 is lowest, 10 is highest) of the threads used for network peer connections. This is the * main thread connecting to a peer in the network. */ - private int networkThreadPriority = 7; + private int networkThreadPriority = 7; /** * The Handshake Thread Priority @@ -554,7 +557,7 @@ public class Settings { try { // Create JAXB context aware of Settings jc = JAXBContextFactory.createContext(new Class[] { - Settings.class + Settings.class }, null); // Create unmarshaller From a23eb021827a8850270c7b4a4c081eeb4b746c17 Mon Sep 17 00:00:00 2001 From: crowetic Date: Thu, 5 Dec 2024 20:08:24 -0800 Subject: [PATCH 14/16] Revert "modified autoUpdateRepos further to plan ahead." This reverts commit 04203e7c31c943d48c96097615d32bc67e318d47. --- pom.xml | 2 +- .../java/org/qortal/settings/Settings.java | 85 +++++++++---------- 2 files changed, 42 insertions(+), 45 deletions(-) diff --git a/pom.xml b/pom.xml index c6f8bd7a..bde846a0 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.qortal qortal - 4.6.6 + 4.6.5 jar UTF-8 diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 0e8e95f0..c3305e82 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -67,7 +67,7 @@ public class Settings { private Integer apiPort; private boolean apiWhitelistEnabled = true; private String[] apiWhitelist = new String[] { - "::1", "127.0.0.1" + "::1", "127.0.0.1" }; /** Legacy API key (deprecated Nov 2021). Use /admin/apikey/generate API endpoint instead */ @@ -213,7 +213,7 @@ public class Settings { public long recoveryModeTimeout = 9999999999999L; /** Minimum peer version number required in order to sync with them */ - private String minPeerVersion = "4.6.5"; + private String minPeerVersion = "4.6.3"; /** Whether to allow connections with peers below minPeerVersion * If true, we won't sync with them but they can still sync with us, and will show in the peers list * If false, sync will be blocked both ways, and they will not appear in the peers list */ @@ -222,7 +222,7 @@ public class Settings { /** Minimum time (in seconds) that we should attempt to remain connected to a peer for */ private int minPeerConnectionTime = 2 * 60 * 60; // seconds /** Maximum time (in seconds) that we should attempt to remain connected to a peer for */ - private int maxPeerConnectionTime = 6 * 60 * 60; // seconds + private int maxPeerConnectionTime = 4 * 60 * 60; // seconds /** Maximum time (in seconds) that a peer should remain connected when requesting QDN data */ private int maxDataPeerConnectionTime = 30 * 60; // seconds @@ -272,19 +272,16 @@ public class Settings { // Bootstrap sources private String[] bootstrapHosts = new String[] { - "http://bootstrap.qortal.org", - "http://bootstrap2.qortal.org", - "http://bootstrap3.qortal.org", - "http://bootstrap4.qortal.org" + "http://bootstrap.qortal.org", + "http://bootstrap2.qortal.org", + "http://bootstrap3.qortal.org", + "http://bootstrap4.qortal.org" }; // Auto-update sources private String[] autoUpdateRepos = new String[] { - "https://github.com/Qortal/qortal/raw/%s/qortal.update", - "https://raw.githubusercontent.com@151.101.16.133/Qortal/qortal/%s/qortal.update", - "https://qortal.link/Auto-Update/%s/qortal.update", - "https://qortal.name/Auto-Update/%s/qortal.update", - "https://update.qortal.org/Auto-Update/%s/qortal.update" + "https://github.com/Qortal/qortal/raw/%s/qortal.update", + "https://raw.githubusercontent.com@151.101.16.133/Qortal/qortal/%s/qortal.update" }; // Lists @@ -292,36 +289,36 @@ public class Settings { /** Array of NTP server hostnames. */ private String[] ntpServers = new String[] { - "pool.ntp.org", - "0.pool.ntp.org", - "1.pool.ntp.org", - "2.pool.ntp.org", - "3.pool.ntp.org", - "asia.pool.ntp.org", - "0.asia.pool.ntp.org", - "1.asia.pool.ntp.org", - "2.asia.pool.ntp.org", - "3.asia.pool.ntp.org", - "europe.pool.ntp.org", - "0.europe.pool.ntp.org", - "1.europe.pool.ntp.org", - "2.europe.pool.ntp.org", - "3.europe.pool.ntp.org", - "north-america.pool.ntp.org", - "0.north-america.pool.ntp.org", - "1.north-america.pool.ntp.org", - "2.north-america.pool.ntp.org", - "3.north-america.pool.ntp.org", - "oceania.pool.ntp.org", - "0.oceania.pool.ntp.org", - "1.oceania.pool.ntp.org", - "2.oceania.pool.ntp.org", - "3.oceania.pool.ntp.org", - "south-america.pool.ntp.org", - "0.south-america.pool.ntp.org", - "1.south-america.pool.ntp.org", - "2.south-america.pool.ntp.org", - "3.south-america.pool.ntp.org" + "pool.ntp.org", + "0.pool.ntp.org", + "1.pool.ntp.org", + "2.pool.ntp.org", + "3.pool.ntp.org", + "asia.pool.ntp.org", + "0.asia.pool.ntp.org", + "1.asia.pool.ntp.org", + "2.asia.pool.ntp.org", + "3.asia.pool.ntp.org", + "europe.pool.ntp.org", + "0.europe.pool.ntp.org", + "1.europe.pool.ntp.org", + "2.europe.pool.ntp.org", + "3.europe.pool.ntp.org", + "north-america.pool.ntp.org", + "0.north-america.pool.ntp.org", + "1.north-america.pool.ntp.org", + "2.north-america.pool.ntp.org", + "3.north-america.pool.ntp.org", + "oceania.pool.ntp.org", + "0.oceania.pool.ntp.org", + "1.oceania.pool.ntp.org", + "2.oceania.pool.ntp.org", + "3.oceania.pool.ntp.org", + "south-america.pool.ntp.org", + "0.south-america.pool.ntp.org", + "1.south-america.pool.ntp.org", + "2.south-america.pool.ntp.org", + "3.south-america.pool.ntp.org" }; /** Additional offset added to values returned by NTP.getTime() */ private Long testNtpOffset = null; @@ -411,7 +408,7 @@ public class Settings { * The thread priority (1 is lowest, 10 is highest) of the threads used for network peer connections. This is the * main thread connecting to a peer in the network. */ - private int networkThreadPriority = 7; + private int networkThreadPriority = 7; /** * The Handshake Thread Priority @@ -557,7 +554,7 @@ public class Settings { try { // Create JAXB context aware of Settings jc = JAXBContextFactory.createContext(new Class[] { - Settings.class + Settings.class }, null); // Create unmarshaller From 071325cf6d6c56d47e4ec141c6d0887c6c758e23 Mon Sep 17 00:00:00 2001 From: crowetic Date: Thu, 5 Dec 2024 20:13:32 -0800 Subject: [PATCH 15/16] Bump version to 4.6.6 to prepare for update, modified auto-update repos settings to plan for removal of reliance upon GitHub, increased maxPeerConnectionTime to 6 hours instead of 4, and set default minPeerversion to 4.6.5. --- pom.xml | 2 +- src/main/java/org/qortal/settings/Settings.java | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index bde846a0..c6f8bd7a 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.qortal qortal - 4.6.5 + 4.6.6 jar UTF-8 diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index c3305e82..2a749242 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -213,7 +213,7 @@ public class Settings { public long recoveryModeTimeout = 9999999999999L; /** Minimum peer version number required in order to sync with them */ - private String minPeerVersion = "4.6.3"; + private String minPeerVersion = "4.6.5"; /** Whether to allow connections with peers below minPeerVersion * If true, we won't sync with them but they can still sync with us, and will show in the peers list * If false, sync will be blocked both ways, and they will not appear in the peers list */ @@ -222,7 +222,7 @@ public class Settings { /** Minimum time (in seconds) that we should attempt to remain connected to a peer for */ private int minPeerConnectionTime = 2 * 60 * 60; // seconds /** Maximum time (in seconds) that we should attempt to remain connected to a peer for */ - private int maxPeerConnectionTime = 4 * 60 * 60; // seconds + private int maxPeerConnectionTime = 6 * 60 * 60; // seconds /** Maximum time (in seconds) that a peer should remain connected when requesting QDN data */ private int maxDataPeerConnectionTime = 30 * 60; // seconds @@ -281,7 +281,10 @@ public class Settings { // Auto-update sources private String[] autoUpdateRepos = new String[] { "https://github.com/Qortal/qortal/raw/%s/qortal.update", - "https://raw.githubusercontent.com@151.101.16.133/Qortal/qortal/%s/qortal.update" + "https://raw.githubusercontent.com@151.101.16.133/Qortal/qortal/%s/qortal.update", + "https://qortal.link/Auto-Update/%s/qortal.update", + "https://qortal.name/Auto-Update/%s/qortal.update", + "https://update.qortal.org/Auto-Update/%s/qortal.update" }; // Lists From 386387fa1692eab09470a82c9493ba7e4006ac69 Mon Sep 17 00:00:00 2001 From: crowetic <5431064+crowetic@users.noreply.github.com> Date: Thu, 5 Dec 2024 20:18:52 -0800 Subject: [PATCH 16/16] Added modifications to current Windows Installer build in preparation for 4.6.6 release modified AdvancedInstaller settings, created new installer visual settings and included logo utilized for that. Modified Readme file to include additional instructions. --- .../Install Files/AppData/settings.json | 3 +- WindowsInstaller/Nice-Qortal-Logo-crop.bmp | Bin 0 -> 1828254 bytes WindowsInstaller/Nice-Qortal-Logo-crop.png | Bin 0 -> 164357 bytes WindowsInstaller/Qortal.aip | 1824 ++++++++--------- WindowsInstaller/README.md | 10 +- 5 files changed, 891 insertions(+), 946 deletions(-) create mode 100644 WindowsInstaller/Nice-Qortal-Logo-crop.bmp create mode 100644 WindowsInstaller/Nice-Qortal-Logo-crop.png diff --git a/WindowsInstaller/Install Files/AppData/settings.json b/WindowsInstaller/Install Files/AppData/settings.json index 088afef4..0d66e4e8 100755 --- a/WindowsInstaller/Install Files/AppData/settings.json +++ b/WindowsInstaller/Install Files/AppData/settings.json @@ -1,3 +1,4 @@ { - "apiDocumentationEnabled": true + "apiDocumentationEnabled": true, + "apiWhitelistEnabled": false } diff --git a/WindowsInstaller/Nice-Qortal-Logo-crop.bmp b/WindowsInstaller/Nice-Qortal-Logo-crop.bmp new file mode 100644 index 0000000000000000000000000000000000000000..0b9f457b7a6b3754e36d43ec30738ea4c81ea59c GIT binary patch literal 1828254 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%0D{S15B_#NSE%c-vMvyuLMybb6O{N~c3JvyoKDCr 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

>Y08ByzLAxH|Ahxxa=F)C%WDdd(KJtv(`n^ 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

2sA?-7XyV8N>VZ)kMhn5H%}4&#WMS>oOWKyl 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
    n9%6G`C;Btsh}@Wk!)ADHzz;Df({Nhoifxfcu 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

    P;_++JN#vzR4$b%3vMF1| z_z96sw^WtLDM%ag+r2xcRY;3vgyOxXKQ{Gfs6O!pT3DRs$V&+p7AGED5Y=r-anzBZ z6iO$2NSeI|C6=~Sqy$m4BoTh=om8SmS=O(;T%!6U23=1~FMH~*&6A=>EAsKz|KwKZ zWzXAoOCh>LrnlcD<)k5H$g#z)NP(O0P^P8z2xJJZkrLAig|?(rq~Kf;twzuR&|jY; zj#8QoIt4V0t{eH{L|kMGg1wV~xM&&&UbqQ0D_p2D#=uJ{Z%XRei5;~%J$DQuc{tuY zhA6FF(mU{DIb&^Ie(5LA`{y@av&Gpu;{1-fS%$}6{tIc~JT2Y1qau@~E0DJ>^1!}X zsa7}QDhGJXpD(>$DBa$SgmDYks*DQcJ3QoV-noUv^5FAd{7Z;qsDrBg7-fn9Emg8} z)%F(aj+VauhEZ3F1rC70gQ6%~JB2(VRmNv=Sd#7A_sdVd`0chaEg`=0#UDu7KSSCr z@J$S;Xc?5IWa!I?e!V0aewtN-{&WRLqPQy_d2=)^=U;H9zHVW$j3^Z%30%r7U}DP* z&E+YV5Lvs)ruuLnfq+wdC{m$+=%t<%N7l(hyX*4tAM_(xEE^2ZGjgScg~c#5H?y!< zGLZRprQ19pVK^h6A4tJ3LhOv;+dwzp5s1wQ<4I2fsZU{e)t!)X`C|Fe_4_dRx-xyo zJ3jJRl~jhup6Oolwx7M!x%R3@rhelN8A3)HhE3{a_{pdnycDY-R0s0Lm@kQ^*1A#v6Af*^#FepTP7jfLEAP&flAx1}zQk?KZ z5bxt7z6UBwD6>0nm#t%MdG3?Wl{Yyc54PhTXFv$r zCiPB18mTYEV7=6vb%dfgQdH07xq+JQ<2fZFX92%ZH*L zOW<-hEb_Bu#JuLCcR0-R{2oLVh=fXBqe3NwY*rpwD^ra~?!0$T7RqA9AyXtDwEwoS zIE`?`t$<(Y_!XaWCP{tK&uFQU|C*`up>L|cbgIKX%yp8Q$^l|^}&bhxrmqfAb9d@<~lRPwA%x_fr|DVy+K@;K}DrdlJQMbni`E>4*=}InER+ z1zM>j1qpj43EQKRcGuzhx$>L8`A=7kj$bpsaDnGt?t=6>tt=XgKy=(6KE zvNyYhJb3@!#}=KZr#F4KCGEx;c>0UsyIJOXE<|%A=!MdSa1Lo5MdyL=5d6-M!D$qC zR9T`#5r@hX4mv<;YvhdjN|gmgx2nMQ!`)SSy}`h$i+4u@f;eW|!Xtw6I9QuJ7BGnc z=``~^g2%%=BM%cMf8Ef0e^#^k*GY#%%sQ>Bs7k}Y@R;Ss`VEs3NBh-)lRKTHZ1rPji%_ef_NiXio=;&s()5OT=3zt4arx_mg z$k(Rj@*0$MO&YBbZ(vf|E%G)`Jl9y>EiZ|&??rRGgY0J#)9;uGF^5K8} zP1+(_SS%}Ee)XBZIDKHJ_+YiBAMq)Qq1tLWlva4kd&$w)N2)eMUQg1V%u|HEOYOGE zW)^@SgAa^PZk9a<+VZViliQK~vI=|(g9?2$CO|*QzOhiGhH3fNUvicFnY}>PBfd<& zD7-`R=#X16Sy-IrAV;&XSTd;n=@WDcG5SKf5Iz+zR)Z>b8eII9#DTIAR109z9N18h z9+&kSpCDnf0Sx_V>6vH0#02W&6;Iph{N~R*>Uo!35J*_RRl@FWDHhsNtcXlZBGovN zxSdF^g}{-h#cM~(1u1&SQyfUE5lP^b#6{i=s7y(tR3gMnLJY7vE{M~50x7wU?%BH{ z?OBm^5S^!X9+0RxC7UK(xDQHW>PK?nIi9@fWf#8ZnHQC5_f4%wA7`z3ak}m7pZf0F z(j#!T9v8&%dq*|{!y z0B$GGwR8wAsOtrbmm7I8iTu>Oe&)@Uuoo%olsH8PSVkSeYXSRdAmjHCku=FJUK!z)?aM zHn2=_9BI=cMNR36%TjVHQt^CodLpeE5zelmwaYp@QhO$mGd9%F^-s(Goj1z)X9e=w zpL)Xge)Spa#<$g+H$jf!TpWeVFZ|^+aIQ|1&KYvn=o4gTr;TUi1R}a~C<`R+(;9_V zChSnnV1k|OV#L8Il_!Si$D)~6JQfzqhA)2M_fe5!iIa{ZT2Ms^1Ta-sDTtZ6tcA0A z-BMoYUM4i^ZTyzS^Cx8I&Rr6R1-b0rj?AFW182^aWdng~bwrRg4`1Z8=C}zMNz3 zgV14u4<{H@QLz#`5k%E6BM!Pkgh2=67Njt-UfT7Jw7XT=vgQi;>NoC@b;0w~J-enK zhJbxsI2pY1PhR;1=bC3+Bo)~yYbte#n>)pgXQdLvpj3hc>G!%4_mF;+NPrHDQ+VQ# ziirWzxGlv%i}c3ugKMGiX?)~ct-*pQ&6)co?d_4RYYXz6CtV_Me%`sx738cSMHzNFYBsXg=+*v(v-w&AW@LR0VZz5#ckC(E?T%4 zG*`Mg!F~aZXaZ6~zlFu}qB35Tc54b%vL`NuMWsZcbY|-EB3KbZnk!*MWr`2Mk?X3| zq+D4mH{X1hJnPEeQR1b=vB7bklKTFn#NctR6u?u9Aii@>{-TQkMMCAuO2KeFWj$v7 zb;R{jrA+zW^&eh2*|j+Bv32WO>9kwI>9Dj;A&ixQQLvmeEW$0bR<3BCC9RTMxRzZ} z3e^pA^Bp_oW8dohx4w9)f)+%OH?z4{YL~f_{*o30UmWwH-4 zrMOleJTNDbY{DStOgU%sFWq|4SwFX+n|{f~6V6|}>haF`8s>fCKM=@~lWnc4N z3B5T<0xD8yk%B=cg&l6~9C3OS=gm@mlXYSTBTi9TK+HVdd0DcsIO(|H!fmL>vlw*7 zQsbWH9GE(hT8w(7!}V(76>1P}XJvGBgY4ToC6nV@12YmyldukrHoXFkZ4EA!o*Y_&YY}QrXwy;=9xZ+`z zxAh?7(7J%*a^L6LYmS=k!x<%J;J%a~gH%NMPWhl2;efJ~3t5M&gmA|pCxzQ5NQ$^l zWlFPGkm~q0x$W+K^1-jRO%kUZC>zsKsBbOAm$V{ac%1=`wXhEI)negKfBKv6&MsP5 zoR-M95DSYX06PRKMYxa_zeelDwOI91_2q$!_vMr-G;hS!6uoVtrmZl1EaPj}OSipG zTHQTTElkL|u`{HO?x!aca`%G`2nbiEXKlN7K}Y-gs~_b&|CyKGap5^@r06uIIlEI@ z_5H|JTLKSvg?PK482c$lCk4#K<60Rx6gS+_zi z$O*xL>&iH>rOrI@mwzE8cM{S4-{BH%gq0jtVtP?Z-uUYV?5^$n2Nv-Z@u+4_*qz- zzA${t!eVKlOgHCH(Wc?fz@rH_8$i)|f?pofa*p^VQV(1d&xTtrtdW`h`=neNkDgU_U$gn#K*2 zo%n7j?ZyG=wVUX`5{b}(dC2G3crA;8Tm^F5s#9dTcj7CYNy=e|g$X1f?V;+Y}I_*D!e{aJ~+yCm&S zOBy5LbQ}`HI?x7jq6zU)4h57?QRYw*EISJCY^p3T5{~=I62{Mt``2laLQ)fwK(?7% zEI-z6S+91WYPGr5H|unY!7~-C!^qdy6;OBMP&&B zNWSoeugT??{_-g);GanR#+%=B^ZM~?W@;mwWXGOq86P=c+HJmnGWrxXg8hQ-8$30L z;P8rHX_vbWFfRuO7mh5JtMa28|K-pDEi6txlC7K8NwMf)pdKlOicz4@T7}A$lZ^#> z^Q<@Zn#B0b;E&Ej^9?RX6G{SJoRE$20Kx^ z`KY{5#$>*9-qOTT3b`msa2(3tyzi4A#sv$D(-O-qr5qLu880$Dvz$?bUHg-G^`Upj|+Ay>b!H82=yWaG_gz1!(MxUh4 z_3ZLt==3!m9n3T5>3D#=JFz5cK~*YN_$-6xQJ9z6+EEu9sw z^`Mlx+oc>nBoo23Y#5)PuTA5)Rr6jM^>#`$_d{tNxJlO4>hg@oUnp;S!Nab6!TD8Y zxF6{^e(M7_6oV_%AKrYw6uh<4Y6h|V^zVW@#DNOckZb1-n{s$pV^Y^zEz zR2aU4J|E_D2M@n`asD}*rObWHV+@wPQ5;iw>ERWmAPD-L;-+{a>brU`Mj^PIWx%$F z!E$9(n%q9w8<99}OS9RP@!DDP!1nDjHhzZu;O1N9%qxv(LK0(`7^AE*2*FVte_YWgW{7s;zRs34c&RHTmd z!o9M7-3i>nIpZ4^7AFE*){BgQl{#S^>?~B{bO-G%0gq+d($j6w3eyzCCI7YDq51Oh zw?1<6d@`oX9dy0yBRQ>yr0YKOLz$WAKy(}#ElMv6q|pf^awg?FH$EUA{o3y1&)f>a z$e1rdk^P^3N2>ozsQKy_)Utrn!uney_rbb@yd_0+=OyHKt^De*zT>B}ixx)+T4iY& z!vJO4|1fH?ns7utHW#eY4T=qm=Xm{z=3|O6XN_&R(J`JT9VbtmuQ@n3;SNSfEUR-$ zk-=l|d$o0K1g5X9(|>SpGG#COqZwMC5TB&(ug)2I5Kk8g34Xj6h>PN}dMmvwycinn zUwu8}<1!5{U`j#{l@S7|^dXADyNhEQ@5M+UL_(Yo(vue2Oh>|a4rdFo_1wuHVx*79 zN+gOKU|wA*VNf1;ynBKHyC+2uMLg9lW<3_Ot)3y@LDC*X*(M7ZOot0~Aq=ngz=(W*gS68% zvh9pF+&xlxS^hy~eM#Q^Kc6}3{C_`v$*ZoqWJJy>+$(3vE%KV@Z~5n!K6R@qIfGX} z;{xY}S4_&*!mV=Q8QvtaV4fBvaYoOSN2(|5l8UmoMg zq!i0*Wh!dQEdD~bhn#`Jo?7fyoC;#lJOyRYVx>UdIn^O$jo@xZ+|?(-pM$p@pj*Xr ztGnp(I5IM2Xv&j*s~2p;VXudJ?+bTWN@DK5gm#2Rr4*S)=S1qazX3UPaZR^ThG9F^ z1ldvf8RGf8qD0gMP<%lvOGxB2ijbZm2aTTOvhk}l2lve1oA($9l4u#(G5vJ*x94{f~Q{Y8y`P;w!)wgC*P5{6k(Ds#!vdB7g3yWjID;_US zd;cwR<_2Hla9ZlkmUw7p5$YJaQ^n(a^pSqj6F=>V2jW8Eu^T~lr4sSj`w7-uF4WJd z)J%q5pIQzM6tilpx(;5S6g2UJ67t1xto9^7KzMWy31mCRUnilnMH=ZDa{X;>`K`ab z<=8W~!f@V&Yf;|&q&8ZBNZ5c6`k9pcW*G<8cxg!z;XP3zCGm~%k9ne8Uy*J%mWwWY zvP5A?E_}o%W&u6K$}Kp&Dda2V*0CSmip2YP~}bJs*Wci`4-3*sBu8 zI^=LxDhn{&)cfHN9xp%U==Upd=|9}hsKMPlknv4FtvOYJ@ zn<+~UN?9<3qY=0Q;yMXBRT7U?S|&YqxDdpX0D^i+x=9T}`G`2qRq4eKd-c3GHCb~$ z^uni}=e+r6uXcX@=U?PpxOv?xvrF@E&Eqd~e(jYnbpF>XpYOclnU^|6Pv%VyUAO*a z>0kc&Cx2r1gIp7)B4JbnbM&R)R#AEdY4$KU%qkt9N9Eb%A1_c#vdD|34pk#yJfj#PTBIgy~Is%jKEGE&)!!#QcpVc=78q~N#Y>Z>mM(=5UX z#3KC7!r~b4i1XITfxY)grG%KlO0fQ z&6B=Bc}~l8S?Jt#?ee=s=r|w@;GI$FI1|!#*2-`F{oUy&Z|XnBs>T2Qomag+jOvhK zX^BmsJaQi&mk);j(#;n$^W|e)vQ=)q?MJd{%OmB^A8(guUj4hP{#~35{K~KV!*@4r zJR7B$jddu`D#aPbN&R=8H2{khkL7Nqs|I<6%E68ogM$DbdMqEV4_*|2sHIohdT#rO zUZUGsT>JQjpS$MwoB%`YPD|RIS!r=g8LH{M5h+&2#4YlxZ4n)fr-)|A=9RK>8ij=F zpcbPAAhSFetqz#4_5AX#S4K}{d~ChkbI&f}4iYD7JJTmT`OT^4KRXS(zUR6vsVfoE|G@PuDvNE9Xr zT2_%xNRwRxXkJ??j^eif9^gvgm&J7gx$myKWpe$`rjLKhZ?4qLFB#td{;$1%?Yftz z-}(A}oSh9`%&9yB-h8qxqK*&b-KdX=u9ImRiK{fHp?K)fTb*fn@>3qCu31>DRJ`n2 zo1WKg&Pd=SkW*ZVdYo#hxi4$8RvBjlImm{_Qj)Uja>IzLV6TcIWGsTL8#%s&PEqE% zj(qETcglM{_CR{WUGWWBv{i>^JpIw{O?y4atmyL~&#xTler^C-jsPK!w^nbTkYR^q?{Qg~1t89?QTr355LdwN)45VtfJ34rTi0Qep_yoE}7q zoIoj4oXQ%hhb6iDp{9K5Yj;29AFrEA@7tHYJ4<3!!40^hPggv(N-%lmO6dFYFk=Gd z%kciV8;gon~94-y`L#llvN6dZNybyU_Ei%#g!?~fBw_{@Ws!& zD&4p9F7Ydl6vv7Zc4l$YhDdcnq*jye++N%ScOj{}(KL|jI1Cgj`ZX^`28Ka`RL_}n zS0$KtLnsetOh#&(W#7KK)aN>2{8f4KlVA7uo_}TfxF`G?1050=1h|iV(r=_raAo@b zzyFUd-ELW;s3y&pD}FGB_g9rhJC=H<3lW8-?hD3KC_Pt-KGz4xm$ik(>5V(?e6JI8 zcQ2_+setxEr81Q$akiV6;)^sf?#~ z`+|X;0eCO?F%>Dsih5bcbC5$5^cJZbD&o|5QcSB9msFHi#0?47OE9`kX2J>i{_Rup zzJGt^Wz2f#jGjDYBZKYP?2`K zE;G}+z!x{l%)XY?4>Y7)*d$|XuSq|1-8X&{0WEUau>PfcirzYTaK|p3`REt(Z<%Eb zvWk>d1v|<+3gi_hhf|^qz(q3AMS=KLE3fJ2R{*dgX>sIz5d7z--|0r-b8)bCt9I2Ex;GK?09Q)G6h{?rZimNK{UOt5GqE}Iag)j{zj#gv&Sy(JB z&N+9Tc#zb)oqE<%X~mT(Q}}gM@t$r*8Gl=$@YPpDQbEe)QK{9|;J7Sdw<`zs&&YwP zh7`OFGFsgxH{bk_QmT}S*QC#W_8+WT=I|3Q{OxqxwwHi6J~e&c-~7L`J6-+$df<0k zHatou$Iq2^#6=Xj-j#k#y5?{7z?CR!%UCV54#fbcRvwYrSqi}|`TXatRf@&xz$Irm zS8rNZgY420-@!mVYldo#^o%%+qlJo-XsZ-jQ7<>RK|0r!)49_b(kr`gN1{M&OWp_r z*$;NppMJOf^IFBVtWf4bhRrcAd>0uxq4dIzjEzyIE=Z@lS8acHqHy)|{xIco1Fd!s<+L6?em2D?udD*8ao1^IT&oJs z!Ax)e?h8}tO+Ri*p?U^{qF8OEMsquYQ5cU~z3MHzGzQ8w-T2MoQQYgofCd!CUl~&X zX0>o9b9p_#E0EekXA$({`Gf0>Hb_tV>&j}<#eoT)xoW8qBKIr|mqpWZy@><#pA)X~ zAC8y1Kew7IBt2DLUAH8W=bGu);>W3!!dZFL=5cxT#r<3z6rD~uwq@&2&P*Mck?NXl z5|k!nX2%XB1mT@ubJZ7KPA_4Zrz%@!G1IJM)$ah=d6r)d6NYZCVCHE~R~GWZxqkZj z;MAK^r3gmPIsEJd&;geU=!kn#sgz`Pn*InCa4>`c9o|c;)q=xq0t*rpix~I?82E(} zCtOgLJB?t_R31nD_oUkmA#kI^$NT2MDn-0*)Io~6o$>ktQ8{JPY}3*y%d z)+Y*VT!1t|IVMtX-z&Y|XDR`=#u&n%|DcoJHWNyzSX1Vt5t1ktec_JT^Z1Xc2B^=P zuW1ijfT?p+hfO$oflLx?2K_Vu!#DC(%A`Zbc&jB8026mO9Va#k={xDX0hu>#$qZDw z+2uhnePH6z1iPOGpmq_XJMij*yOG4|3li!qw-M_9Be8%hNr(*dEv630W6(JyCm|IUtv7svyIxCtSdKIiL!ag+>B@RdtB((Ga|( zDV{STg@P~bE_Y@lk0{8<$QbxbMP_Fj5^>iqCzgPgIB>&mU801_2EK!aRBJW4_~HxY zkKgu-Z@%)%OIDPLI!%&a_0xZ|`^KAZadx1;iK0Xb#WMJvC%tSsW3eKbV^$y-@=e9wzxF`m70z(s~*87;2|kJ^u%wq$JFk=eNilnk^yMxsG}sGrmmj_G zH$Pr6Zv^{kX0M2560nN{`-vcvLDZHiJj3o3LPzS^D4Vl2#dtohXGklTYN@PTeO;G< zK1WFQlQ?%ETYGU)`7tdj`BSfna52gcK8Y`ZQxhMJ!v|lVsK)Y`M^E1Jghz~DqH}rV zC>EZVmMdq<>}(6qvsUf)!slNR==X<&c8idYJDn-1)ix>%#8#^<0c7bYjL`=|wkWi4 zeMZUD31nj*vNYs8B`Cm=50@@41Rx%DXPDRxt)d9pP?u)$%09 z8a3IyZwJmXhJdV~KRK9ZM_g6u^ckw)vO$lfWAO?liMy#3F}{hTS=qQr;61Mip#b<@`rWelN@ih;t zOrc}4mwOh5Gj%viPR9l4pHC;hR+NgDyFZmF+-{DIrRShRqQKPF@c<1 zyZ7#seY^Kbz16`W;VcQMs0Ru`NeVu9Ow$Tv)=ke1&?fbCEUz#ro?nn&Yet1HqJQeP z>gpH|p1A|8Y()y;;|-(VmX4HE^O=({-YcbQM_P@qE=lICGR0yk@rNJ0J>6XoFh<>k z8dgDDh9}q@y8u!F=RS||ZmCr0F0(SlRdN<;pHie~Qpyx$iCO1ZyoVqhbQTNX$jCcH zIZ0#iCX7)ET-zMiAUeRsMeuYNf&&-erTox?ET_TL7J^Uiav+QwD$8V(WSXAz3B4eNDQwj@>QATO}p zMLby^SY^ryR;F~pfG7rLGJ7AxN{~S(s~egL?>nY5W?1^0cv%3p8sK2c6qBy;Gnb5i zn#J#H6>d{1G#A9xLmc(eM;4pZ>R(ggU3DY_38E3-zZ*jgScK50xbrOYRXViie(Hu zdk}cz1{i%6gD);Prt{^Y7xB2FJA~mFE!XTTg60wvIY=+yG+f>@3^6v-aZ>CWeUH1Xk`jIZFEi) zh71eqyBR~~5kZq>g}gSHPR5`zU-ioJ>~Nq=$-?(c_0rcq@v4TCS?i{_)kxZ-r3UZxw3BL zJlR*@3tzlor!G#REhz*^$6bT6TB*vEyqkxK1F>pBX`1h<48vm(!Z@f1v?x?@*k1UK zQFpkIwhjj%KLL2R7kEBU4Z#f^{j*gT0RzWOvOCf7W$IJiExf%@>5lpnD4~^ zA)7ljtzaH#hZin5T&I%2I%p{u#4 zs)snpx<7?MOJN9&1$ketj11%6B`h#SUI z^&1#?J}A$7?#1cz&!v6L(czls{bBl<&wf*?B@8&b=n-&#)9CU%YO#-qO9MWFDI@ZH zejLn~A?~kaD^wn3LCXFSJiq@o7+&{ZjHOi#DwcYN#r=)Tc<2l9Zd8A?%3SsAs>~A9 zA1?5mm6LipT0IiFoYaRTgv)AOc1*{}8z&!1_Eh;m9usbZU6girpKRIWS!K!)^jox8 zGO$vF81Y3Q-Ean=Qia>Q1*4O)uN}(|?s-7|>4u$OzhU2zZSVHo@BX!urr(0xJR@m% zK#DHN> zDG-n-c=mTOs-Q!tNTRH8Msds?WX2#ok4wSdD89Q9vDPE(I*Gyw>9xnD-5ilty(ZUx z|1SCVckYm{e*ISY;un7?pa1+#^4ZURUq1br>*X__{*HY8E4RtFzjdeF`27dvj@$Oh zj)yujGwn#HU6!y{!$4qM(wH=Oo-~qHI3@e1@0O=O^Rcp2iWCcrrN)b%FtXwNt!rdv&wa9Hlpo-tc_IqO z?jG1Mt>}6*`B+w<1%kJ5AnGN^ks{;|`K5A$+{akdkTY@M?MMis5|Z*52eokWgf!+8 zt-1MeCs;r(h`bHa6zGzJa{*jhO(YV)kBVkmJu?6Y%$hHH+&1YL@afO|u2U&`5(Kn} zZXj>G<0?RZoRIYisvJn{)2TQxE)^~2oaLt#FauY|(=NloRqtT+x$2qN$0uA@kspNg zGwSCeSWd(u@0fDWa>_myM+N()mKfwOWW1z4uK~;@mo8shh%p8b}5y*QW|5!os-7g1L75Y^(DVtCKbV? zeW4Xluh*1Ti@RzVusY<38O~>V)k5`h8IfseGM%fz;G91kGMKP=4B{_Pp!Ag}xYk$R zkSr`L7DKI8+I!g}FOaTWc;uX)3y&q85D1z zwGH_Ld5gKGg-b0^qJKzBk|r6^5<~#mKqkKlUnNx@LKAWvQy@zqjB?sdh_dPQ92qHX zm0D%1jDbdLTV$lNNvh?IQY~(jVqqird zKkkyUbFM+72!4IyIoIuBA5>nYr2BFG3|fK2kpeKIx*LDh?-fbq9l zTO>*(4)Z)}nP7P00_5AhIi64Yt7486F9Q1f#ylUDD(ol7vFNAIiB81nI6a7-6h>pD zLKVC=kaitc>YX`hwCXb3nv;5?DRZrcG}~=ycH7eJMG_Q7rQnZ>>(!8-vP3c0eDb9Y z;lI;miu|gIeu6IUJ8=2s6~h!I&~icT4zf$-F)4c%^&HGE`wNh=w!z4(PlM-JsW9g9 zd2s5FrbJ4}elb89I8C|r_J6xF3u0lhYH{Wo=Xb7nVlBzQijC_`g6Mg_Q{^Ykx>4GCQr5?w97gT@x3Dx??A`XC!WK9UYH!IG)N}R)#jpj6tRr*WPos{FP-{f=|?d6Uwfx(m0SlZkPpSt*O-a+Kmo9C7i$ zHiT=Da=DVl1XM3I+~^Ihc4$VWJ6^62FrS8>Rd_D0z?&PzA+m8}3$y*LWMgYwm3Z9Aw z;n0u<6(sz+$}f5Ps~wNIctI=;T%Cnq14YpUgX*AYKt8Dpz8-^@$Ks(w@q$=@Vl!oO3qH1s85!(nWwREKU|)`P_}pcqNs%wO`WStde`Y z0)Nz!DZ=IVI2C<4L5`!Q!y|KwkhcWqc``73NDcHsYKjizGi9OTf}fI4()Doefo~eu z?ZTgHb3rEVyRH5?<9Or)T+<3^I3B`yGw`3oImkhLWyW@wc4l@>E--D>O&+cQ9WX7xVyU1;*t$_DMG7cLAnbyC z2)0VAjkmzvk~jym9eYv-|7fNK34<|-Yn$ZWW*}d>bGQ8EcXoc}V>?pr4YM^%{h+w_ z&W}22`hCcP4VjtUC*5vSeR-%klEi9v zD~F35fNOE5ikJ$MFdk4ZLi_-C^dRW_ihMT!6%+*?&5x0D0HbH^Z-Ub z51+=YS%7>i>L}Ng!)n4np(3(r?RuGNe1?_M!s3*|^PgCAFFfZA8LM*YAGc)(KVnS> zQMIaf)mT1|D^kfpow`wWa0eX{cj@8IK8nC;XkACQPmu9qP)`EsxJ8h0^Hk2Ll5NDDHkp|@21Z}DUnRRp=odbeQ)2h4X z%l&&pd2kwmYHMXzGm&?G{)hklpFi5QAmzny{kQ+f*}Ln#&SM^Pj`%@We7`N#s;4Yf zBDE*H$&aCdB0)Xb5Od_?&Y#Mn$;3DDxR$jxZd*xEPXsJnUY0r4UpdUNro+ROn?e5g zM8KrK7!Bh{!mL8{+td*}Pd}`wi9qIO-o8OUwc!_im=`~s&cSd5xjiZj^TLI^Wj^w} z@Njw7?K(SICft78KRC0~cgx20Yo*oLg*@{CGwZn(4O7mGyzzd)=C2vyi31k!RXih) zsPy1Dk2&o`nX)4&xA9K1G`Lx><3KRQU_juiYwFV|c`%r|qJk!eiVxlgiLmmhB#;J1 zOs-!LFQ8I{{#kzV!d+j$VP+A~?5f%3_2F|UDUeq@=-n&ohWuzfd2iOD`(rmvmY{2heH#0p)#a*>CaU;Z7Hm`?p66c)Wwe}}wz9zYW%33Y#bv!~ zXRbvaNAw2y-t!F1fJy|VNMId7Wl-%GZvM~1fLpcYtS%)Hgv1zwY$RyJ6J;Dhfe^fb z0wZa7`!X$6G91FFa3-FnemL?Ml8M9g5P&9nMnI)ChO)?D{OsQ5{wBRc5yRDB%Mj(#L%Bq*qZl|2m3d_e#3q=ap2l6Bei7}`MLHwpK#B;^ zM1+PvCe6wwxoKY_Z~NlC>9_W$w|CRSFH(EM4S(*0z0W#ZH@h-7d!IyMLn_rO@`!SA zsQMTv5)$btVxH93MF^ECcX3dARaVsw41!|5aqZy}j{n`cmdD$IZsf6OR};(j?14BOB53PqZJ(`nW3 zl(N4a{AVwUcUFQDSH^F#sKIl{T_Pz~XpzD8nXl(mHq58vN3L)UGM+Mg?**j780a(x zyhl%^A;%%#VR)q|QT&uU_XMbJX!l4%@fLP8G5AR`AVS%3@mYRSm&r9FaJ#5`Gg6xfB#!q>c~s;Pmz^bl_RgQV1Q#tVPD!k< z$$QV;7^i2i31!@$k>1Q6C399VHs=RE#p+zherjC{wkxzDG#x|ikCfZ@W1FK*OQeV# z;aI_Ts;9=oh3sp#M^?KYc0=Ba_vJMe z=~2gVz&vyxtOLW8L4{-Ks1nM~d7a}1L(ET}hSM4*hRMfYO{DuuJ_q}7yd2d|%-C6v zo6Ru^K^2C}U>U)g4COLjX;)&GYqw3whAp-1qJ;r#{0W7r47$P$&Ii!9H2!41d9Xq) zhK1p>gABvx!`3QC3Uhyz_smr|$o={Sn4S0Unx!*bz)CE24C!DT6A^TVNwFlfB~A^Z z_A;TFp<9-gHzIX^T;}{q`H%11bKwVXY2Nx^C;cM?KP>LK=ii*$Z~x~jC)PUBYCa@m z>-hPhjw@X$_#W<=O3klH1&p~B&Y+`!pHl!)vjYYRAz65AMtf>hYQQJdF!>)YgOvup zx?!7#r(od_(J6O(rSJ}2saD+i`N4Qf*6vsjVz+r0@psC~oru%GU5I@!4v4Aqf3 zHyGwcCkuz;fx|H2O}N8_UrCs4{kK!^-7l3=DAnqy^g1)LX5BVi!4NnGpDK+=r?pQ? z6;5RtezT@MJ^IU`kAcconL6+g&bujGfvL2@Cvo1L&5+LHc}Ns~QJ>Od}b{%R2l* z-hA|6+U(2d=4kR7>@!BTkWC$V@=8z&t}ysqXzoZ2D| z=QtiWTJ8TBpPs%~iWOf*CnlxaI-owbFhixH>HkQ~F&&iyz`Tr;j0L=(`8iS|^6sfY zN|6FAkiibwKbrW<3Dm2?h{UZ94ihysn0te}9%TTFJQR^n>EU3pamp|L%Wt-uTx4 z@x9r7*UQMpNUD>e^rBtjqYvmsE$K-|*4NI)Jx9ci$Ix-W74))Hq$q}s29=@{$t+it zs|0O+?ZYL6)|=HGieXqB8d*8_i8FJv&wJ|iY4Mdn z@5zA!`=ng0;S6W zIjXHXDC*HdF3lDiRgwmHDwPWT-3|N1l=#CnOFTvT+~QbKD*3aUwvMChJJM=2alRh? zAN#ke^tzO;NnB%#GB}A`4!n=+Tg_Xu!@}a|5pecF+7mY@3U{6@7K(Dl)=eiwQFtmr zOB)u;3iAbdF`$3Slqs5c&fbgyVJrpdIYsGtC2>aAN;onhcTYFvufK8kzy9No52O#o z={s~P2gkerc8^`k@rc9c4n8fGN{PLGu^<-*hMtm0%ze2t%V_PuFMz#%$)r}mFGsu7W zRL7YWPv0F}=Q=c+51RP2NYT@&NK~5qj$^rveSDJC3e*Ki@M5EXo?Co+S|-YJYovot0#if~%5MGsM;t0u%B8wAyE~*Z;Y!bM3up6FWkO4s{v7%w&GKOe>m1~pfxcd! zMjKXB;bW`Vae*&g4F>R%)c|pbRCjt9QfXLNtUi4FQ-9z*^{G#h2OqdiYPAt57AmrD z?=Gp9*q7H;)ppb$U9OCggA(pp_vu=kl28K$)3Y9gP!H6)H0Yl+C!{~u_VUzU)vw9a zfjv?P5?Q}tQXahjV|MgmaoXawFIxY&3(lU9sXe!&b#UDiv=}y=OpF-lDydKz5Vs0e z(_lzCsKp7yO*eh`#ohJ+Df)p-&(2A;xKa75DU6i8UGDxyIgzDLr7{&Mw7#>JDHg{8 z4)M5yRWFiWrz261R!|nhAfL2YM&!r2d4)LumGJJqk@oNKOxpyF6T>b*oBnr>ov zQIOh}ZBpELwrp?4^5q}hC4c#Y-Ra&~UZ-Op8Z%R0a{l=vzx!u%?d_5#d&H~sREO$; zVe174vqe=?cCx8BUM^#Rc?7x)t@26_i(`lC98F2)N>j~CCX~375y9|=EG$+xzWUYQ zcCLK(71HVKMUAe5W~AAfl3HO-dtbn#0rvk;0 zg~hSL7030(<$mc=EMa?A#>b5{utgr3v|+K77?uUI?x&B#DOAh?wFHkT7n&~sJZ|?y zyi!3%*Nlr-t;kd>l3fRy(w4GRHf@s|_s__CzIWeWf9Sz_x;HtzQsw2>KKU(a`*Y4? zuYQ!o?K`DdX-UazNI{y&cN+|x`w`LZ#1oGTZDsqytL4m9hsuS@m^VA1Ke0fel2m7D zYSH1y!eaH~3)lTG=NZquRO0x6oOjM<_{>S8yF4v%0oLoY%7X|1+ZyM{`8k# zwBgZbZ7hiI%!%i)%}}XAO0xkwc`T=ZgV4UzPMnCVnj9}@q{J4B6N2x3@7>Nw#g#Q< zBXXdAuaxl$sK!!IN@MhYaJ8)_VdZa&V+U(d;Q5k7EvZy!4IInk9(%dEW-%W|y0BOV zRB#K6V*x)@u?{t5 zVthmj#jdw*_7Xc%jj$?QYwx} z(XSxW%s4txYK8C+D1|Jg`CvWB*9D73pcEl_1xiM0ut@STuq>gBxgG%O2P#b9)8EB$ zTWYmhznq858nsdVt#p|7GoR?M+ug#ps>9^lp7tMHn@{@M!gNUK;$;VaL-X%*c>jaZ zk%l+CZg4)FXHCOA@l?RtH6!bNxKap;YTJIa1d!%~sQu!3srbGxwaQv`&fVr})q(`YP5V^ppY4P!EEd6JRX+5p7oB(AXk}J9jr*il=t#j0C24oX2kQ(tf-tfwh#&Qm6mlVi zP|t6p-{jZ5yrSFUMBtu#{{1Pt5Bx|rZK{gvVtjyJki*_eX$<_Y3+l+^1h-9|0+IJw zSS*f#B*gyHkd})a3{V{|E)5xK_s;ud{W=I<7K`G9C{vaKr`jN380rIChWty&`xAO9 zPm*mzN2bX6k$SxnijRs=R=y$(tr3J2pi@)Fu`dD5Fyo@cX+?V8xXk&J@?f_tAHQq% zHSfP^I_;!#DH1+xUD!YM-_ASV`NmJC?yR&s`=wg;q+ALxjG09T8cDm`ly2CTN_hmK zar1auSym4ZL=8^$cYMM?g$gZCj8Y{VEE)>qwv>xwcpsBEM-7QXDVOaPCMkNM)>21?P#BFc3&ldI{H5u~_|J-v{)q?ATrD zE~^`M(YImnhR&u|t%&bYc{T?=*pN~=kVb2lj8rCMq+FGHW0!Q=+ht-+LH6$Xg!A!_ z{o!HzJqwG|8JDbgu71-?E_nUfn~SpRzMG`vbY#P1NxJn|I(0}SXhANBZUN#+5P0Hx zvBXJ7dLgZtv$kt-!f?(x8^1wWe8;XErC5ZpFHPAnu|XQ07LI*cGr3NtrXP?>Wz5Qd z7Kg-OO4!128b3_5yO6OquNB|#$;B6+lU=bG#)=ap^5g4MX|PQr4MPTpQmEvw8ToNg z9_XBC6_O@MPm>4olSBfLaY+U6*-Ge0p@TtohX#Qq>6FH0s=P+-O3U)SW-7n?t(`ai z)Al4r5Gqv;iU0M#UiQMd`sbZTJnAf&ZQm!I?ta-gzD^2mA~l~|O-9mc@0L=jEE~3* zD`|A-!HiX?90^!E46!og&mp{c6)?P(G~4_NK}&_6v|7{RL#P=W-vB==%vhXyxapP; zIM2K4@$%5F?@OuLlTNr_W?J_uYn3%4V-ianEreEF7N<5M@W6sns5F{8C5l@zfq_M- zT#|bIK`D7!op%vtS`!Yf9d%TfAixm-6qWg_euT0 zE?GP7aVij-pCny4JtdZ+t&A}#c(2gLDO^1091DLw~-9*r$CIH1lCst~&#bBk1}+~dBE`?qDh zHYuG>L-y>rTU-daW}3ue7AnS^%;IRlcdRLlr=?sfNVPhGcfdKyGoZFqYM#^^2eNP$ z7N;IQ{n@vxE!`*Akn>H+SgkBG&4*-v{Z8?uC!R~=XHN7v_@BjUhXWqSK|?A*P!rF0 zWO{nH^umUWj*VelH-iwf;yH8jrZ@b|9Y4C|pX^JwusBL=sLH#4!tgKP|07OGsAdX`RL-O2$n)mgJIZ0 z#egp(|3au}VK^t@jyVO4iL(W!5)XCU!!;^cvcLeR1aZ4$wPi9ABJr4sa%U?Ni?!1O z+wF+7;I=W&uB;oEc2JVL4>aUk_w0}l{Ah2wqbf+veKk7X8@#o8I<8~PzcVzRXNwk(336nWoWjnDrl@a(wHPs*t z8|YO+85t`}vE<6!>~_?FU2^u>Woh(2<-F^!UVTv(#lphk81O6Cp65K}@#jgebr0l= zha~XkKwT*oAxoCIjtXSdD3G{QkTitcsus<+SSoz{lfUmg{F04Qt#)PY1|O2*9w`)D z*|6bUNfQY9{i%@_7Doo9TA_l)jiu8)Af6w}e|_@**ot%rn<#R}Zi(OVJ6RGxsMj6zSO2b!kR1ZlC4E1%dFxwW{^cDzzb|VhC#BnK%AP$t zrBv9As|Vwq78Yox3Hwex4ueEAP^5tPfns*{0g1vJZ6iJmR#YonOrdhB;7~PDjg@nc z%a@XWF-no5lqj0i?v4ykMT&6O3Rhysl>`)#&;oj%rjGAO-H$;cT@P_VRnQvHI^1L( z(pcmAlFYSadTLG@T&#BeSu(SBt9+{-$zOc?q2KuS?&M~jlSAWOfAz~3wZbnrS3Kt7 zQY-{gM2EV6b{C#qKaCx@90OFvFp3kr_<6*w2YccNB^etpOSip8=IZ<8%I7^t((V_W zr#$Jr_v6C;PC;nC=6Q}p(KOz5D(9Ymo{UYd zLEqSv$3E^dX*aKP-tt>7zgh*fu&_8`c+q12+~sMGf;TRv7dg0sTj4C5o#F?f4nb ze7f?tI5bX5E6Xbi@-Y6Ku3|tXidq|F5e!d_8BTZjTuqVT0n+2Kn`CYlZaBSz;b1C& ze*U$ilp91pAL=O>f9 zhvI?+RB}8ic}4Nuvh-q4W-5H#-czdHtU_ zjqX1?XP#L=huVR(+dwgJO%)FQ+8LbpUH#70+Co{Jd@Z1#3A-p%;U6l%A&}jBKIk{!e_`^& z@p9*=K(1;u!|AAu>cd4qK5)9?W!FCWEwj^~a$fx6$IZFkj5zLGMh#F40z(%im+_bW z%h2Vk_)w0H2quliT##w^hjm?@AMGy8+tSe?kk8*Sx#0QP^Yq=t!U4K($hoZFY9}+~ z(ReC8$ajZ#=rrLrW$n5Ngq@aq?)rwDv89H-^1q#Lef4)&Wed<{#2u-K?AaBT%gvPe zO2WuS$K-R0zYdC6t=DF8T7f(Ly!p9jIFEnCnX+lzkxJT>AnHf~RUu$A%|s8@g4~OCK);+v?yoCCI{zOs#{nMXv4Ht(d zQSHfE%ifk&yj`4i#COq_y_S6Gi|;yt{b(jrak+{GK);7RgxRFL>ym#k%gafC$`4Ks zrk5}{^wlGTF$pr@pnsV9{V<5uxYS;&J?SGHB*-YZ6_qN6S-E(f90Ku=4`Zl*JtO09 zkPk~z;gG)KhS|M&>~Jv=oGW=%-G4wd7uH1cCr#W$KaF)>NyE=YzNrc6>N=r_QTeP1 zDu<4P>Izo{;dLgL1vhEp&WdoTC_<8b!W&alcyNVrV*FB`p&-MT$!Av86^WC$n~&2@ zBmh4xAm0Vo6VE{ixFIO=eCa}1jS$NL6}+O9oq|+jS4!v)!pM`^^=HZt(yIL9?NdMT zSGTv*n_B6YaI3@O&imizeEmCr_^LGBA$|b^7&Ocz?cfS?&c<9u8yW$aoVb3tA|ogH=pp>8S!{Ps?MliK9dQ*JW#Txu7sLUNm57{dVA|09<_(kXu|D+ zt>Hc4jd~z<&+0wmT?yCANefC-oF;hl3CggDgexehxQXgKhkElBXV;E@=iL?{iMvuP z6~yz&5^!G+^hiu!N8g{McP!9(B5A&5h((5QhauHjWN}P7hVu6! zaY1!HC!dXE!kKIv>OT&Gknj+wa|j3Lbj3$L{Oe=A-p8Fk{^OTk7x=r-=iDQu;;fX5 z+(NRBcGDIg?LxIJa0_x?6r8wzRs3KKZdn{ZP=bi&%BX*MWr}#5apL|p)xHrn@2WI; zokA_c8<)JTi?nd=;hZk`Uf-p%lmlx8HrQh&^4dHt7G^O5*#0SJ`FIZIh51qn-TZUp zT$TSUT#61PZNVkaZzCQR>+th=y$6510}Q?x&ntK+XWSEUc3w>1jC@?7|1j=@ZXEoTjb_aa25{ZODub z=n`~}g;=A_+Jj8bCm~PVwKDR%(hXwifiK1E%T@j$b*A8F4%cpqDAX*(Yrzk<8CPA?Z5gggZbLu_~1$;dHhr(jl zES(HeeFz(gTIivNK9+;4BHk;jW<|>WT7)XgWO*%y6ZC^TBn|1t_o3djrQY~Yrf}k* z*8D6%PW8}NIpy$3u2r=Rb)qc z=;=D6<1$wqm%FAr^6?+;d&URvXs7#o@@n4mPNfI)BVz{c}dLKO2=h{AJVdxcu?47Y1 z>U~DQ%Dk%TvhYTwT&ha3P-0gN)(2t2FGx@u#Y^!p5Nt}d=*Z|eI>h!)aSJ_p?OR^? zy|nqMLs#Q|Jz2dl?a8!Xb&lhKf97VtN=cC&v(D$~$T6r_M}#9x+vIhC;o3U?b*els zMixhmx4h-WSGPM~c7E^oUh~bUw@(`F2c=XF(cTg%fn4<5lru`@Q4HdXl0*%O!YSn@ z+}@oUE!w=g*}LN45pB}-DS+T*sCo&P)rC?5>!13_0O2!N7xi)G_P_oiu8L0=j&T)H zAwxe?7ng#_=(A(>bzwkM1}T_)D@`{E&qbH#%pij?zZht!1DXg!aCeIpbR;EsQHX*+ z21+SLRIXQ&+Soc7U%Lg@B8htt_NnNHFz@;hwzwlmw~qc{27LwMa-;%btIXtzp>#U? zq}ALm-QFH~`ZF&@F@49m{=0vxN@~?ZDXTd2)L^md0ygjIB)Y5z`o)4WsJg(SCh<7K z(J@zxQw&94Zu_|xOgg{z^B2DPyfX_DcJ7n<^j#8QfaXzQ3JSn3N?;uU$K08Vrg+k3gF{n02zo|`?}z>>Q!7pBsie%zEo^$h7i2%@Rm zv4SEOF$pQQdtl(-7p3~B$FN0<{u=3e}^siDioARQh_0y zlM9wxcDN9Z6#PIu2o3EHxA&w5xg?Vl>!gL}?sTVd&K4BQ`iY|SI{Rg&y;B~0#S`Sa z*T3h8%H8o^oR*7cNEAgV+b-@ED1};*#5nfh4(k7+SWwZA2CV)3x{86Bi)Z84P5Pyt zI7t=9iPXx8)Z6!>GyhB_2s{?RJiO!Et@N(x6ocN1#Bm~FFUD~}x*eo6ziH>n8LdW{ zabQqKi7KlZhTk2G?=1#ehY!wPYU?zONs?yIiS6Znl45d$nkx7H#IQ~$` zhg|>;h+TA0sgLm}-UGraHj7Lp^hfzP@Gp3%qumGOs%LDI=Rac6W^~Ho!V7*jec*w; z5G44OguHb`Mn)&4hdS12%pyb}qZ4anZe~V24)oD(xb0`GR#jx(f^Y%>MO%#FHL5T3 zBo-fEim99&IUJ&m4fA59XffgGBCY=UF`(b!4X2Tvs|u96Hm|~h_c&#^dpY26+j<7Yh{(!FuR( zoUUr?y>1J)n3gr`*2(8S`{ysa{PJxdRe?_-)J9&AcDkbyN29vzl#qECCa`4SSNQCG zrnHbmTT%R6*D3WS(dkIJ9D;xEmTvg@p~`G=I-(QGrJuXLedAr*_seY$&Pbs)Ddmc; z&z&xKXyl3)l%-hEpT9_C7wU^JPBXQu zkA3W0((m1HqnvT(Inr)~vTte+d`sZH6N>j1yiuuE#!!BaG@DJ7KgF=D-K%G2`5y%H z`mcQX8SvcV*l~R2=LP&Ijkn20rQQo<78#LBu`B`PzUkI3_|3_~&plUW<{pwA`+kW2 z{N{Nr=|tf#{_}zK?j2}P<;~K?;F6*_Hwd7jk4ip%@Yf$+_+gzE!DzY4YM1hYsaGe~ z)pGda*D0%4(BDO^{c^#XfxPj>>ke8jjAgOPp?37iMpVWncx=cZA1?1tokG7|u+}}yTejlr29UQmc^_1qbrNz5@M09607k4d54qJ-J}+J-iq)b*+9?GQRzQkz^aex)pH*N@2F>D#5`Hc(F2 zJ4dQW(ct|oEG+W4tUkALDL(XRH&-0Y8~(|kb6JyBfX004pMNkl&Qp``M3ToO~2;s-}@iVsi;UX;FmBxZ3cAMcej)T^xr+- zD_kWuYSB@>V!UdlBs+>8$>P*TDUh39_@uJ)>KC5zvS&TveA%|vlW_VTncH)Rc)fix zQtC*_X-c~}C*4j@TJ1mePsnbT9@E4pelI&y7@&rp%+NE1zL^cI0YkLA-MkfcQ|wP|Jty3@2xU5 zbC0Z<1ph69D;K*`^d0HNvoc+OKno_37*S>!1Q_psIzuuEXS& zCwV1`@gH2%WRCf>rs_-8^(6{JX?1&2M+IvH6`8G!$o}f6+|iBYOZPk^@4IW?cYhS8 zN3=-obn7e57e4#WH^;pPW#|6yOWAM8`iZI(D61uzae_-LNt%fBSq2SIWh+m(JRnYm z8jy;o3vbz=-vUNJ0JwT{2FutlMjj8QmgfnWp<96#y z42eX{dp5j{4!imlS1j`eASmqH;(Pu2VUj zCM`+gj-)Za)8eVX$6Ut~4+EKiR&dhAlSW!?glp(Yz?GZQ?ag2iwGZKFkvAn#vK!~y zA;r93pZYQ<>Swjhn-oLQo3Kd)Avc(-YF%&CFNo$#Xo_WvAAWV zK|Y8Zvx1Z7C0uMa5H2>`=a0D7mryhQ*-rd5na_$qtybFGZhzhRzkmJae@7ZZZvm^3Az}BRAK#ngdtF=tF{r}s06F5tb`p)wg znfcB-`|g&yTdn)Pk%SH;kN_be4k3^*hrvibFgAycy|u92-u3#JkFghD zYh%V{@BzLC+kkB_9u8x4pO;S8_rD@C?ud-c z_x}GE8M#cZ7#!krK#fbG;N`xN1ZqEHt0Pw;%mmTy()M zQTL9eAw|gBWmj|Cd8HzLxkl4ee{d0zKC9@pZlkpiYrbCBEz3t#qVSe7q1lk%F;MFcT#s%^2_Ize7G4jq^<=M zBh-gOMd=SjMyq9nGn4RUrm(G2!bMxR;nk;??%UvDO}`x8a@D)jZ$GpbhZ>>W)OVs> z+m7iusZbPul&n*&V2NtM=?92_adE7 zHPmXOcBNi>L4P4Na<7!@N*@Q3xt+?y6ra7;n{r>%Uo~`o=qa~`Zz)0V4eF|b+BsGO zi-tK@SG+{xu5yj8h`UI-%5e8gOLzI&B5kWa){%V~))r|ybk&5qTB^E<+?&;aQj#y; ziCpJ#sK%0PrH+QzOGA=Y`zF_JsCxdrH63?L&Ih4IBg5gK4)k8M}PE_ zfqV`fNrz8rMn+znwp&Ap!eJRPH9@)0_Txk6`-5%&p z6S(j7=Y8|~GyKc*>?a(5{6}B>kMDc$&;8WQlzu){rEj`dmE6N)HFxXPE#C=bAoNkz z&Ny_&K0_nyU}UbPFlcr#^*-^Uy`3L9i(@@FdM4_%5iB%xqj%jdLB2=Z4fl1mt-ics z4x^(1u6oJ&_>0f}=i-SwCj945?N1+nPQFQ0wxS_ZLa!s+JL~2bP(GIyzZdBr#U31e z*WOzC#H$Wm^F{Yv*pKsfSMb5RH)u>xXiO1Kcw>rdUOp~LXAG>GDu0Vcl#X9<=eQVF z^eg@1?7I{i3n-W63w2FC@I)V*m*Q084JDcGL3rr-eK>Q+Hf#|8{Jy;i=4Wue9N&E2 z#?M_=H*b<;%M|2ZoW0}q>GRLGFf_IY3+)Q#7F5~OPeQc}s$yJhbNA%P2T8|H2h1Jo z$rf2{C+1^CW=v6ILjG%t_*7A{?Xw~wRUNp!`V%>J?%sx}iM^PcdK%Zi^hW%{KYr|p z8%|@2Cd&HYu7!U0y#ZD`C9d+Nx^5gunm2}QsUWbO!m~!&-PhbAdJAdRye&x5G zer7KwCuR^vF`~A9`E=+WA-aKLS#C%21y#QE>v`9EfSzAH<+@3-UU{CJo9tN46l!+J z?iD&~GtFzwo+Z)3q@{n@;M)iV$>D| zNWQ2^ct!4K<-j1)eNJLgJaiLENjg40j6G-W!aKhEHGlZN_uuta`Mg=>2K3@$bmaE5 z-5f$3jmVhklL9L4Tht4U&OUuplOr?uS@esq+|NC)iApU**g7ckKaO;=k{1cz*`NN( zUw!4tAJ~6r0kds`d67`RE0k*RN_ClA+4m+-?w{F)^4a~;&Ox1J?(X_Q_9+N4-2(9d!G%BhdU`||Zhz9!3O>63Wd8_xLZHD{V@ zY^D>O+i(5J^b3FgPiVv;YThPn+`LWh+Yt^;&&l^kedCXw3njny^Yfvjk^N@mj+HDt zLnAZO3^^X?=X4~WBMp@0gT7u15GAu{HV>mdY_NH29gltID8I311%Ba^lbJE4ItH0u z)|jGGMXymid-9g(^=qe9&8#igrp3s;g zoQ%d4OWqGXhgn^j5hWX+&Ur1K<6`@}DTXbklQKsu9$Kv!bD=)7DvG#Vkose#8dF5C z#l~@9`e1@m)WX)`DymTf3lj(AD`1M9<8{2_@@;pI`S`Rga21|@;=r|+UGUvso)JB7 zw69ZT_?=u_*MD_nKnlb0f})qBZ@&yXLyC&FPenp> zW2I;o8@6vmYheNlGf!j3*3H=a{3>tg$(aJ~{fDQY`Th3c{pR(TUp?*_Pj}8-f-!GS zYqis8OcC6*(C@z2jlK5s3)$tNF@^Nw6QB6IU-_@U_9s8^^fS+4>hLt0ZQY<#t~co8 zJTm-xACvRlS*ha5*vp+#?zh?fEx*sn8OV-(DMQk0ekos1_291NcgOmBPcc40;OQGP z)JP)764-IGdsI2lT%irul_&a{p#OBmj2^ez3-VbUbww}Q|-gL(2uQYiP(s70v5Y*TGh zqTC+_Gr{O8N-%_Gqw}nDIsLh9GHLrWe>%Wgu=ZXdc_`J^e^}0_)#+(|DA4?%4LotQC;JGnB-A?w4HtnRS z3K}3o^$_DHIUFOgtexfw={#b@^dnKW?;c%coxJvHGKa=njDvtT)NYhH4~uNV-_{3M zZ#|7Z1I@VH{k?HUgzAo8WN@~Dr_ub^Qd04TW!{Y}CTZCg=G62g=RAeqN}9@1(JvFL z&vOpiC;)hXqBWmVNYz2-?K>VWt=cUR1m|4pGG|T8a8}inPL+Uxo_1I>CIhQT)BxNG z#Q@=8I>fmyM;Y*(2>Fd~y68oonA&e6MIQpUH!~o7LZRu!x;68P8gTK54OHu^{K;9% z3}L^{R92YMCAt{CL?q*c^I6%cw4a+a~(Zaf7E9v>@bkZ+1Q>c6df09;^~UKon8H|d1Ur+ zG0yLX+>2`MZ3zpB zF-s!9Zj13yIhr#fM-BQf(_NSYPJEfd72UCl-=tij^3-vbr z{&2ryjV~&!3&*Si^beog_IEAw6nYPue->Tdrh4aI%QRc|A_~k4W_RyXy${|Kv&bGz zAgI%LbBNUyWKC?(gvF00hLGmZM@$EFqGuMHwlimP0@g&fs!2wKeRe?gEw_d3S22zv zQBn)3$NT~qXSW-gs{=Rt*NoidmQ2i-(&DXbJDuf-w9+L_rioe{KQx}ZjHTM< zBcJaS``bRVcAIhIdme8aeR{(;?+3r0-ypTz1@iixn^O9WIAtb1UB+D(J|TAScyiod zVkr058i4upfc$3uH%;JR-|V7CCc{dESJs}&rXU1_)#8bu*Us9@`xOGRP1P+l46~oOGCPcapU=X>$TnE||gdEYPj(@UPyeL;@ zf&C_s;R7@+q)BMr?07HLqL$!-yZ3y2iv$kF8}e%G&sWFciQ-*!dnfWh|1=i!7bG+% z_cXA5%`19x;u_tF%7XSzO?sdGv7U6KQO5h>YqlNs$|6L|hW@xTA*u(@Fde#`*^?X% zZKvld$kGtHKXIvl$f3LoRu;PHTQ5{rR4`Vma2z3(Doy_}W~DQBCui0^yVo_*50mSb zvmz1BcV_7e4>S6N5HQ7mKvRAFizLXl_I+4*csUw6dOtVaHD@}f=oz)+{q8=j{qHr8 zBZ=1_fpjcDux=@ff^YP5Z4HX?!N=s0M%>_g|Jt1`mtV$gr*{sjBO9^|H=t^p6?5g7C!W`Pn{(Fch%jC#@a(0ZliaczjcoFkvm zvGLk;D4YVJwE6oyd_#zNb|luh5yY2xY)<7=!DQaYKYCte6;0cV)VjIXFhB-AK5wFZ z7Q*T5i6N~tUT60|=N0+3ElGHMU#94tbvq>L#qdynOxANU>mU>$ws2*#9Gmwev^OzpIRSEV2h6=m#cAdC~ zP?d}y<-h(!-Ey{L0JSWfz>z6QwF}^Ol5PPAa3IF@0gAQLMF3;^SSajr{ANU^kOD4?IsyU(2zX&}s9zU#ozO%@n@IP^7B*t(P-8WpHN< zAfBqcJKA{n9eS##mBPx@OZ&~sTH#{|mYCM|iO_O!#m*V^nU_w=@oR)Ztv~N*VW^Y@ zH)+DL(%F>Nw+-r+MWssJhbcTCc?yfoPE{5@#FrTN47+*3g%-n=47GDc+)ndz39=qA zdhJeWp<{`!#yPeaCmrGZwZCP+dGGM72qIHiF>j^Xo-AO@&NSSY-XYab!HVP-q&K~< z**3iZ?Ap|Kc5fxe%;Q*)8nnz74msv`B=zsR{`MhG7bynN5ow~J>OBRMLX*@t4sL=w zwT{t7M%fee600A$9CUjA01mQU-3RAGD?XIKGOLm`sCK zh+yQgC|jyWPodd3TXzi+9p0iuCz~83FzvS3Pi;F8a|_ykEIla#W>z1JYji? z6H%_jHuCSL`$llPoum1MRz09iNtr*4;qJesNG$SFhye&_G1?@LhMwDK^fOQw?F{df z)X0%0rPPSeCk~q46?_!(_-cO#m5J1E7s7scpdJzNd32dL=ZiLG*5LiAN?PaL%GMpn zqOfl1K-TO?HV~su-^{VO-NrSu&hb3S&ynwmDu^8en9gh_ftj1{RyiBw4al3m|DUTv z{zxx!Tkqv8q)H11s6E_+rUEB(zm(ubR$shf)$Sl$GUo>&CHqH+q(qv{P#rWsO?{<$ z8AU|`MW4B_vBHi^+Kc!?vui0C=YSAtNl>&(kv^UMiS)P!yoNjm_?tT-WCRV}a^jnh znh?3F_a0l*3l-1)O@W>7Ert*cKTQzW)6|^R#GDbv5ZACpG{nTwWYrytq|g)}O}x7E zRCafMrI8nKIUgUb9U({CF{(l{{CGaTR2A(3h@6aW<{haEFz<90mQ_4H#7ls)%@)eWl!OrtiBt&LBCg2(m5T7OTq5KK9qo^%gg?FUbl(p%_P zrg-H)?U9*C&B*c(!lFxE?G;KYdH{@^b!&b!BW)oUH0^`U=zzVFna2PA9_vg@+!T)T zd2DoS1ZZINgD#%d0xJgh84EiNL}PR+Xt0cOhghVgx^$=`h;*&(?{X+5pcr)PQ=&M_v6s zM&HZ*r#NfL01KssO8jDLlb=plstqrc=Tm!{#d2{s>3B%LWD#lTYk0;u~ z{>^!jaT_s}UA=1Rg?`CrVH;45t4GT6$ay0Kr2I0)0Jk^Nd&axP$}4h7mw&*8)|_LAs+-s{eC<0EHaqg$FE6!C3YTya^SnqmbdUz7><=1P zdqY`uT|Nf7Bd3^gH2FvziN4CC;`LYhIkl8h>n~gbE0}fNVb_~jigK2N}N$EPcK7+D2ZLf1QX z0i0ZsJo~IpG4Yj@+;u3--UZsLWL5!Mh{<}%B)1nT^N`Xo?k|W;QV3&Y=_HJt_l~K- zk~#bN@z(T0HP>&}O^f*Hp*`TF`C8=GN7zPZSR13Z{gf2Aa=jVg}HxCvd z$h*0%oBW|~KvZ05B&`d4Ad|x^ydaqhoE~yeHgt7peBRbV=ytfmTF(SQ95Pk;WWQvw zWKsJ7^GR5K%a&N%y$MLsMZ>Mhy3YW1g_RpT?#ZNAw)Ln`2IJ=OEs05aC;%loE?8H4 zqW&jxgggK9Q@`<)I)(?K(4*;WuF?DXDQ#naHH{aiAmCI-ZV!Sy#c z9C=Rsr3Te&KNYtxV0-)Ml43;YvVXBQd8K?4*xjQnvui%akQ7@Od^)3^xZa<2mV!b7lT-QuBRC7p`VRhs7;8-NT)3d3NC_a^0`f#}#)w3=U zgI{O8w`GD(gp)?hdFF{tNDxW7ZGAE2)LpmlpQ~gdyW!P_??)dnU1cIl7|d+*g0cJM zJ;UclL#akhN!qeV4Hb}_ZdSoWtUSfiBCn+8yeb&J@u7cNlv1W8)&qK16cN%EE>#@6 zJI+7{&pefAd70yFw~8-%U5i4#R$h08&v$%(81QM~{*%khA)$%$8W6kw+H|7eGh#9x z;kG{U{$5RSUOk9#hi*)OkF$P?>LYb3E681EIR9aLfjy1*2cvm1S8K{UjX{PzqEuF` zTHaZWxgZ-X6X!ir_CM-3XW9X)ZM_taZ(SbTEFbG=fC=qFSv&!k35^$@;wYL9PA$Gq zpU~Bmbf%^LajaW#pIG@w9MU8stCZ2kr@(tNHKhIUyEn){5cO*D@(U^RHmCZaOZ`H7 zBAMleO}vx)Od8@1Kg(SAhgK|0RT@tYNQ9D@A;9dI7w1UAqBWw)nR%T=I#HMR0b5qw zjTRY^#kzLPG2%^uvGgu}lhx zKB#|>1asILXv?Th|1fdh6!)Y#MG1y|sJi1>*BP08_Pzsg*h@Und&ZTO2GGcb|E4>dev?H1Zzj z1~(t98oR4ff6b;ZCcJ-;Ko=2t<#*_`$Zm@vXA3NPEG9#^c@gBPMP>kcZId7d|d7W(Tj! ztkXP3aUlS7{3<#pB1nkZURH&2A^L1IZDX9E6s0dCu*~dm_^|`?g`ppn{Y6P<;-WM) zq>Ic#^-9>KYJq7`wk3B0`r-(HZmBTjq~BO`;F6zNu`0fN7| z;Au*)0Koabsj9``a-6cek)n^1=&A05XJ52c}cv$)^Ug*rRnN-E>u{U5fQhuN1Yi!uS~;iCDwNsLKt1dg<2y>6Mmz z^M(0?h2#r#(WdqUq@B;^*N&~MZp|OjY5!fJLcRMhnV`>Y;rndoU$$nBpGYd?wplb2VV`=`bswBw`0KiDB+_kFnO@B zlvK0rYG{SW8o_39n53uU;?4Xydhsthepg&we8@Ep zO)$61_VH{ng{w8r(W$!$?tijS{=6&`xL(bn=49HCz0zXcS^{opO=+M{7q#DrS0eyK?#w40?7KgL z1u+Mr85yqznnIXLBCP&Ai*hflL0*Z$Iw z1seTADyNZLbb>X2M|)d9P^(9ot<}*wh|Wpa>B*z+{duQyrm~3Z4Jsz<*C{Y1eB$o0 z`PMPu;#|~r(4VHmo1K}03AV?p8O;03)bHsZWC5oi1lvce&u1K5Ie$6o{#(P`J4KSgWG20D;XegU2sB@+;SMWE?MZ40zR=0#uKUevV_# z)G6If#}|L6W3$-3TBtG~zrd{*53#(LtQ3@zF@l(mD2H3k@IagVFy}ArJzb%pBfhrB z`j74O+vCzCyS?Wnh_ixbAR-|>QuLEH(1!qn@`jEl`t59^Dd9brSii&3cqk*JfF+o4 z{^hcWGlm8_Br8)QpQd_gqBjKNZ?5QrVUG3jgSyKV@j_do`9Yk>_&I4Db6(EF)gx(6 zy5eY=gIK9hQ1NJ|-9l%y25P+U*U&h&nwp>^n1Y)Dgcy3oF8q zerdJvm9tm>gJ7-W99%1ZXAh6te;=YtmyYi-GEFLX^nb1!aYxXKX6Tk!uGH^8Fv+$F z5}sz;*(03knPXQRy`}T70pk=(2(;(wTMOWTB+=Cl#_M!@p^ryb_!DlDmt8UIi5Iqg zyvW_Xc1;U{$lW}w04aL-iyaL(Vc~3y$b$9vw*c73^HOXy<6+94$CU8!tC7_a%EhN_ zWnuoDip{YcnEx&BL9Xa}Cr9?uwOev9Qpe5r!LtGbWW{0)bOy#I{X&g{?rBkB(ZI^R zc(gkqx&q2S0}c&X*7ulO)S`$w-VMoXbGk0lr@w%_oPZX$x%R`%p0==XcemGrCq(o9 z_!Ek%&aX|iF=vHU&bwc?T>#mJ`FzGB%04?S10c2seOuMz*GOLD@xt>-X?AgVe$}dV z0jjbz-(}DIg4DRJ_8?jRN+JTp-upSWRHjm#k^(c}s7rwVfr|L=F`g2>SPfZ5HRe*C zgCCl%$$b0Ff=rH-_Vuf0KR zQ3I}h?{sDB7~_O5eF~Ny2M`75AElV=Zm`a`}xGtJGsMDY7cj2IV;a5 zrE1eMy+_zP%#{Q1#D5f|27LsNrmCsEdMCO@nJjjHBI0P_^)2Q_X-li3Y+>O7W~C z7wL}9PPVrKGKNeopP+Q;{dS22=`#Bp_b6Y|~XfnLR|HhMqV_d5_I z7N~*Ghgci3=Z3fm)V$e<@?e(LV^KZW(CPjO94_B@oEmj@eH)bIAUP&{Hl}EqJ0AP0 zO=I;Sgpf9NY6#B*?9jBIL`5*bINoYqVjGv6=18mBRK*Hibw_id4pwy3wlhndKPh7^ zCBpAv^Ldo(b+r!AT=Sg@2ho$XVw=lL%Kh@QX^ zp)q#>*MEvH;-0o$ctv=~bNQ>#?ftT(=ZWBaMKafAFS`lHKB{M$moxWrPTLp*`kizq z`fgPL@+;hibetkl<0x#yx#|fDR}DJHAE>}2>LyYf6R6y(kQr_4__L#@4$}((`ufS#vzq(ehA?lxzHQu zP&t=-)|8^LW+~ES8<70}GB{0!G`~!xaam9ihbixV*&a1A^i9btSlrh)&O)5y`vWau zvSUyhO15d}Mp?M2_Or<&ZlhdbDoz)2jtBcf)6!Za<1W56;a|Hsz>Qn^96?-oV-Q~f zzqow;pMZ}@}5+AuTw!td7!AFqvYeNj-c9T4{utACXJZ1-j;Go!~!>?FK8mAdk=6^3YUrH06B zenWTOzdTGb1bpzjNVHP@Fasg!a*YsxdltkCdU1T?f;AJG48~gc#J#bXB-%X-kKv#%ex200*IopOEVEYN@zZjsE2TOrSou9GzzPI*y@8%aQ+w%SD z9u2w`=CvZX)(deZ=CpYgNA1C zE7r7%8CZ{hsO8Ph1UA{zcf+AfTpBf$!)|_Oi8?gQL`1InQ4$eF6Cg<(Q_4H*RH;$s z7-P|9;grud|Ex%D!+>*Ip1@FLq8&09ID(td?Nz^g zYiSm(NdQAd$90;4Ex(E46(ba7s8H;7&qjU|=%_7by>jSAr>?*X=yC?s>f76qvE^z- zuI2_tMo!D0+5J~U$K&tVDiD{GP6qK}gbKfIX(ocz%)N zhxa8kg52GRIC3R`^TjzOprY&u5u2QUc-UZDEQX&a>Ahiy)6QDe>ksntz}weFNv+7i zbLwqfoBaL5WvIF*oLMc7H0BNJ5K+wR2MinbOZ49-QZ-DuS3{yBtSYp~-8SEXqAhh%|?c_I>M-tdTQ6BZHUU0n^*^|H*W0qKoJQgAD}hp{u1bfl7Bs`}MW~qE2r3aS&0kyM--}#N~t^~RZO-S7UP}KtWZ(M>?t+6^ zmD^j7C8rFp?#pe8c^3rFGYdr>_ZW z|LHJKLgZIootIh}uN&FLL=Dh9owR#>e(7|x;BE@1?9rxNii>+%S8souYaQwkw_$Sm z(DCkR0e9%4zTN+CJ%xW;;BG_MFD?)tk(ifNUjIK8;%A$3dh$J3YM{Dl9o!Ue? zRAha1Z4S-p`(UCVkb`f>9{$HXu5 zb#_K`%FW+8;-JUzWrHu2n%rWPHl9Cs@<+++V?&{nhSO>RN7$}3QMq&S95WX@F%7J+ z?2SthQ#95K+Gr#|MszaV?Cj?Rr~ zVMi2hrjea(Ysy*iBDe6T;e1Gdk8{OYz|*1n?kd)CGPB>U-lvDPW6?0+&A91==LOd2 z8E-CUm3T!kcGu9GO`ScvUUpS^yeWcvwKwXK*zfGQCPG`vn>uLpRZS&hpIK-)`ZK8& zH^`E?n%0p!@9_afXBQ;B%=g{judXBFz1HA6tfhAXdv1p1V1G9k+x99J3J}H z=j|<*#=%R?JgejKSAT^)4jOj$g!41c(Ei0T?jPRUC3fUJYymTh?8iRmb(n_!%ThOo z!D;h%3@y`W?)KzWFcyT8HZJ_#IW{5Z+fZWU>CWiafT7P7=*{xS_yB4QZu?m;%ryni7Vi!q^yF=+^sy+ z-`t#f<9=W_Rr$_1*IODE(+$d}|wLB{qnEGS$|ng{{3xd zTtUD|{dd+G`3mP0zi8?!57{b7as%_E;U&S8QlHl>8rikT+0ry%wESwcIWpSRhNh1p}SlM z9W4Jibv8)*+U*mVnBRYdoxu||v#wxgWn|%goWR5_s?~pHo84h*gkxbkHI(1H?EhF+ zpPom@8bPs*(p~8o4)0EV1hca}7hy7;4=0_iM5Tj&TOe*Nhtr7O<@2hNVoy)@0fm3H z3=^W&f5?usJHTtiHf->2#d})Wz-1T-Ep#Hy{|pelYfA7@1qV{6h11RbVqP^!3#TJF zP$xM;;iDW@WFm1miz<$Z1`V)0@4ZP~Y6#=25@va=5Ju<4;kuN*WQrI(?fT5ooc(Iv z-hz|-H=<93X`k`l0r8OCgqgBMf>+PxI3;8gO#KjIhMEl$*}ykddP$dY(;WI5C2vmp!xlNC}26_m8BuLMkdH8~aQ zu)kqC9jZEtjur4Z8LQ>t>qBC}WM&}il-~}w2~pGbv?HZ!XQi`Z(;wzF=-vAy>_oe! z{G=C`sr7W3pDtja!ptvyIy(7kH4{HJej23ke=t=~fCkqMDkqdE6jh-WpvXnQxd*}F z?D%M=^QEh+yI1;o^X6n(Fsh2lc)9G9N#Pl#Lz{#|g^SO75qUyvE7g*M*-~o8WPQFx zGCrl_Wj5~q?+=Hrr2DnxigxbnXTB#N%dZ3(!1v@<+|K9G8V$@J%^e7!4$N+QJ0YaFBsZVge^bhM~%#d z6hFlMpx|ORc)L}&HL7Rz^&AOvJ(qS+X#`}#^j>i;!n0_)v}m#fIp~Ekk*R;1H9l&% zCZ~i1rH+OGvt&J1HZlLCo17|zCHf19D>*H_pt}>R5!js&_C7jl;67c=Y{ISaVbnJ` z@!@QlG$zr489`n;J%Qgo?e11s{XponJ{l!ys-_zI>Wa{hn`gsBIpss)EXd(zuZx(0 zpx$-roN=x6h`2XVQ%#m`3^Qvx?O(1)$)0VWK?98F1 zDV(}Y!$nZ2<85W4TP^#FtC6K#GuKCH5@*IF;JSbqar%DKahRA=V5?MBWq-w!T}u}r zI=6rc!R_Z{Wb2tyzux4jc)ZrUUK4x7*XqGn_06MQ&j>c|S2im!wUd+{bjA*GOkc3! zRl)GJBP##wP(F=3VIwuI@<{`Q!{x7PmQN4X@>3mYz-cQ?aK^4=eZB6#3lQsT(rqr7 z2`**h||>`1aCt%{3+h$Jki;Jb?rzvbkeeW@4J|JqN6{csdFuS7MIY@&@tDr ziEVvR%urQ%O3VC7r)ckIdrzt)2Oz4{zcB4W9Q8R~z@X5;nx z@BC;fpp*;7mH`8LEjcYcu2tsxkdbNG4|e)&3Q|onR8sJhFay-JrLEE~rsX&);${cz zTE(<-eX`x}v_YgHV7M+|m?lt076Xoxn4_L9FwIUM-b#}xP^?hNrO#cUK6Guokqz*W z%5##G0FrA45)L>uhguoGl=XQD9!Kj5FJ$K|%M3m4wXbh^(joXHZ6)~TXqTNxNg6Vjx!7xQc50rQ-CQFrY00sS@org-8M%?mHJC^7qaOOV)I1A z=i=-`!0H92=J@4|r(e0G1-Vm#gY=q0S$r9$)Ti%_fIjGKDNbPXhw z$}~SNXXEhOnTg3%9>VEiL~Kd;uN}X%4G{5p+d-pYrg==Q-_gsITQHPsMOAfA?A$#*BoIX87TmKM@-^`=ea9du{hF zdn!UoRsX@OaI(tmJwDUH`wrfOoU7~Ua}e&|g}=O+S? zSjKRld)1L9AG`ynFf}l%N`*HVWT;fpICpE`LnAafT?F8R^LTFIbZ)yo;6j0D@r7y) z*#QhL=73k-Vd!e@RW-}`UQwI)xlM_e5)%zPgp`(5cL^KgH}C@w^_#7l(aeAa@1pto>y)Y>h9Hc3t)H*po`H zO1q>9-qXnSe=%@gMqWP4aB07&(2h=Dat2Pj|8t&L))Gt#&7-C=8~^(@;jQ_B;jSL< zkdN(=o{v}^we%%+{zP13<{FbF#!Cg*WRNXs)U7yW;LLgU>>-w;Adkd>jQW;yT8SO!1z32jr z@OkfcpVJ|8okwjQCAw>LElFtvTZP=%=g1mkFN2g8dg-T&5h8$`~NF@RX!rHxF?*7fN9D?5Pkc0+ij z8txa{S5`{Utgxb6j#tG?8!RfpApCt^sz7j8o2&abBvn^J{K+i;N9e0UymyYW4Q8s6 zN`{u-bWkfQOdueNYg={gaZRV)DMNrGjIZz)0eGJFVF2GJbtX173g|oeGu6tM(&=I_ zHPWwBUNnJIV4On=uTzV9ftt3oSMQE12Yp`^h}mxe_YN(SaEdK=WsGu?e_%4(`@-Sp zF%1UkIn&jS1u4-&CUoQhgUXEOK?NP0NG-+5d7Mo zA3=d!V(pJnMb1A-C#1B{sMhr>5j*dhHNSb^&2;EZ5N77S-Z`_rNtguYX)j+UYuf?Y zxJrnY23BknFSv^~%2YPL%J7$FJ>nC0rPXT1>a9#ikOq*S8zD6r85dH`OYV`BqUjSlXPOa;eL&sGXFABCj{%{M@Wq zC6QFqn$87W<29U^7#+Kl=Nx;9v+F)8@52Z1njWjP5Vgv?g!!xP29qn|lkxd#LaYdU zd+2^rCc9n*IicJ0aDN4Ay^~k=ec`oR{;eB9sT63A?71var3E1R-(9f1cZrcfE?xA7#l^vb&XYLkb&y0 zUKAA%G80%kdhZIB;+Wqe?5Z3kuT2(5+y0x9*6tSZ>Cq|gjtUSvRw;FN+(qFL+IXT) zyfCup7(+v~^xRFOjPl7R8?ybjWWMPGHM@i2!Bt>`hcf64Yp|3+z4*gWC@chq_^pFq zlt!?2`#K`?-M4uHOy1TDvX*V`h2v)L@YObk|DJMW^hPSCpmp;49E-<(Fn{hqHN+&aca2OPmrAa zF|N(k7DvY7J(+ZvS%nl>k*t#wKc&k9ba8E?EXQUxa_knE4vFN{HE=K&XYqz4yJg~` zV36W0q?o4PXj*%YTbXQ)o<~^bbjNZ8XI2Q{uH4Npi9Vz-z4~O-L4Qv5&*XapX%Ty< zk(TTaR|`Dp*X@TT&#P)$Sc>Sw$1;3z+#D&y^Le%kEJm{&jn{kRvZ*@(oO>0T#k)!i zBkcqP6QxSYcTq_A(qlj_=CXGzrP_0>zSai4EN|Gx`*u)kFeWM8$nCr};Y~DD$O$D}0!u7ZpyGOWmrIsCvh48Tp}&^Q1&0x$>lISf zJ%x&tw0j!Pk?M+QrO^1$UXK0XlWuw_9m*Ld^)?U~ncqD4iHb8rqVmdb=~QSQH5`le zKoTXK-AMm;{f&RxkLF164$XIDh<(;y=}@(OB{ z`8#xb>Lg|6==T14xpvOLrJ#}S{~&mNY`&u&yyrO&SoFh&;4rLxBWU7RBZX-&1A*qC z>Dg_e#KS1XoB6q>}{gHlG8! ze}T{So?D$$X58MJx_I1M8%U5xHJKG{#Abph(@&Vh5!^Hw^=MD6%tNJr58&KkP}7TI z`(r9NQ^d4q}x&bkyf8xo$t<^QJ<7o-mvXH)Fsve zK9PfD-wn((QxG1lm95h3=1ojdNaJcNd+3AgRZ^YP2>*4L`o*My_2xf}Qr2F(3hNt|`Z)J1?8oe{!#{WQ8<>tim6HPk9aE3(# zP?zsZKKWnOYR6`j#K9Iq{_hiGap7reM5SvJqW3+ATjU-_Cyhh3UX$ap8tkE*7Fb`u zkhH!q6_ZV8p3#4s8c{T3YGLQ!^QNZsI3_1f6+XC(R_1@uFc*gO#R`3)hcMlq*w)}@8&dY&Z41< z=c1KOtTi05{NO;GusCCZH6k49qJ()8E9l|B6{ydf38CJi=K1)~y+Srarhl`pm>=Do z=nu0yHktS07Gf$>rTZgKxr$1aut==Z5a>;?n+Ny3n}-%MX9#yC@lgLD&dut9c5x+m zqDvT&CPW^JizhH}*~FrB$I$vQ2PVi+Jntkokgs@KR zSN+mh({z=IIX%C)J=uE?HaO>gTd!Ysrj5mYwzE~3j>BS|(kL$}<`PiQ#R96+=4$Ao ziNYa#{L-T=IZ=f7kiD&2N%EDoso0{fvVcSnr#P279h9D4>TBIZPxUmz0>WrcnNrpW zJfbhz-6Psb(-$9IjSDcO+# zq|xbkYP0T~H$KLjLP#ok^yw-I=iU_3v{9PP1jXhLOGAEKk@b-!d3qR?F%OeO)(30~ zB-d1;DKo!JxY=}d?~u4K`hg?=DyDjOBSTYrq%{i}pg9tL^R{(-vJkp{EKA+;x#RLe zrct#yrlIw_+I$^MaJqOZzKbPtqN>E1jgkINRXOK(3cUK}`uf;w<$22?H=!f9WNLqkaa`>}Iw~&-lAScGG;J!m(SCA-cwU+29#ON1X?!XJ#Nb~RRR^vGb`Qxu%s`DFP|OV(ytaJ#`I&~(1}Xr>rU9W=yQJm z5)IWis(L5-a1=F|VtoHB=J(H%X;yEnBVVhbocvvISh5j0G1P874q76`9yyToIn%W( zp(QHrB4X@5X(c)&vI_aW!((N03^z~9LlUh3B_FI~U7b>@dOuIf8}fhLJz#t%dsG-1 zbG`mK@0GFyJ_8GUj*O;QGal(n3|f?&lTp0pqw~5PTMdQ&%!>vd0bX&+PkOqGdqmsU z5=P0=K@-^BWO!s( zOps(QC`ZZ}X4Nk&LCKqyl-W&oFQ-l;7KG(8gm44}30_9cd@si7s8y+$2$XB8nHYzQClyh@L)aYt{hG>3riZFwVe6x zU1eH3nn4pUs}!-!O)FC*|Lh-5v>en4v@~K-0QOG z8m@6@Pt2gDTanlWXRi1v2L$SrPdIS4MOF+iGjBY5-)-@zsg+;E2kPxUsU2m5#YR$8 zXqTx8{)T7x?-1#CPNORDTzl91*i(h}-K*CNF7_8IRjxqRW1D%JV5haY>Z9{Wmvuib z^V=42;hzoa;$GEln*XPbn+}Xuv z0!VD=tYAVNcOq!@mbxj6P@85G(al3?nKZl^Jd{vsoFv!f$8wrM`ogPb<*vz!`^#HV zB{d=+LW`JuDhNJ6FW#uL>AfPZS%dG=j1iuM)$K>I$VvI`BWp;X8xD9?nmc*jSYxe9 zCa7tqYbyb1Sg^B`g{_#p=_%}U<}ZJFjQ`8Y5|{nzVS=omrYQY~kX(jB1%EY8<7`^I z()@(7k{P_+eZ@gB(nh2^N{g_Toy^K$c;N0TitJIx=KWQY!l;OVn?G>R6KgcZyj}vO z`7={P)2bnPfP8m^~OeJ`bf)e9|auANi=9$;C}tmm$J zX2sc1fM;*xr9NyihQpCW_MD%l|CzreBm|4$!@#aX@6h_reoU*1B(GcE|y0hBk?pPL% z)?@QWlkou(H;?gD6t=yVp{^5}(URzl4>a+QH{+P}4w#@p{XN6}0uYu>&Ez~T;7?fLQ13c!alR5`P6ugBAM*1{(*9s144TJ=%5?bh zr<`aJQL%tWQ^xEE`jt1cf21lfTSpJ3>!I+qTytbK`XP&cJC@A3tK`iwuHYXsFh%_j zvKWlS)gh~T~$su(6{nj{Q&NheUt)0CN@>VKwM^WW7*Nt)dQ`u86-n1DIy zW1)#lsV+t-kMoImWN*hi}F%zw`lGr3BSs*~m~2KPov zHBk9;x9pDSHF60pb|C)4w(#oz5UKY{Ue6H3)OJQ?PQ&)%>B2MnGgI1RklY)sd~W+0 z14@_&W(6ib;;=yFAN|Iva8#QvPa`FBKVxcv_J6<^)Ihfe2WsZ**U0ICwCWY$SZD(e z$P28*uLLsJ?=^Xdk_vyU!kHhT&M->^qVq_@y%$u4lq+Dm?bX3k2<2Gex!XR>kl{ur zB)~PufjiGifhWhnw^*a9R~3(1Nsn_=Ocv+QIj)90fM?oQ;*)xs?~&7+)sF`L%XBT9 zS1vi8e>XcVuG4OvGSmOVkwhkVEw z8TVRi%01Nx2L!y7%kukBDpV zYOSZrw=>J{N_2E z&hi$w7-)l@whP!4H<7isE?A4TYtFJf@|;W;Y&u(G#HUjT8*@Sg?vu@x0&_|~J&488 zy(w_pq(b5pf+A1QtqWYm1v$j1k_wA;{{)M9mu-erY)>1XMmgktZ{ zL@RdI2v1>McmiH7YnS~h=feHcd~H%O(|mB{Fn;%q)O*3CckQ1ZG=1X{=-5QUTfbgs zeF435{^sZ0pQ<*8vN@yw`(9BMU!$YhSyF@7X#vK7>PQbx#V9Ory26!nWXRP>BmNjS zsSa~eDFG8tGFt4fK=DMT0;7_^qZwbWQn#m7apRNvyhbVshwfVJuBDK43bn*4(zNf;JuhM~5Rir7FcC5aSw#@=oU?99hx zCg5V%8||&YttzD(E;qM|H>$&(PLt;$@Jt|*sakv#3sJrvP<*Xal!n=-PH$XpT|U8t(!7S#K8^;9#!jnHnZq-uEV} zE~c63@@I8EXuWeXhi~ei;L0Ef~T9B5_)tl>x98UcYVR`Rd@{DUkmLV9#^HaC5sl-yt>*yYC3p`gca~dZ)ePgd?mpzdO;y7 z1XL|4Sp1QqPG_GwBgarmNEIq}p&7!3D5iu}GM2V}ajvUT6O3{_(11{%Ly*_sl%CUd z!&I9hzS;VY$&|4S7pzAJtx%rrf!ARv(A7dF4WKrAMTTi=v`380Net8+S~rk&I(u*N zy}`X*W-Ib9%UU`UiECQ{j{34Yb#&p~0`_Xgi9rXtd11+9ld`*6la_7gn{&hnmTE#7 zYf0%4^bn-(5dH(tEub~=XSdA(rmm1BkG{0>CNH-^+$TgXx%W<~FlI#DklC^K>|2c` zpL4a5G*8oy>)9a~92w$PTQ9uWS+uWNH@?SkqDouH#c9X$dd09Kp-cW~4JK4tEBQ%H zQQDv3OvRu+9cJF&pxsFq1|^$(W6=lsUO*rAMlChGzmjf-PTpQWn6X^)1{pi{nu!gC z=*Nt1mtICH`ph}3?4cF`lUhqxzfWno!T6WG01i-PS-M1mCAOCXYkr@alU1ZSLF{5~ zOvO+^QrAChA3$*eBafIW3dJQz6kw;F-hb7-&&nCeuGkG?-zM$c{2sL$Y*jPdw0Q^l zSh!YM^K2nnFDIv^NQ(qodG+ggNLl<<6ILZ!yRem``Cnm+ABC0#W#9Kez3GmU_swA%7^jc-K$9H24StOI65X{XoBLKpx4ceLjn~(58KGy`h z4Go#ChUizfHJ$PO8vuXffX`18{MN`?ax^hGYTYl%s5|<}IKw;kYU`RJDMJLk>qMIt z8cgE=Yr2Ik?BRZt_(KJo5Vz%djd`1&VpJo(i=`wAfe)AhiZVmZeO+SZ`WT}0oM9nT z<@ey7!hXtmw@OEAF7?SDodFdH_(*v6#ne*}m5I--&Iv1Paj?Id*=oeZyCxCe1g(U} zcBSS<2Oq_2GZ;#XYrn-;M4M0vVA@|oLnkOL0DwTDm86Oz8s0DgeWJ zg3dfl=4`7B%wbcJsIeQjp5qE{Mo^6oIViYT3znn+w~hm2z!3xpCYaRSw!-0>3z}t& z^y%L}YLv9Gwf1i$BW)<6(z?XXeI@CGueY5xwYfBy)T1NFwe>!M-&AGNYrz!olnlSj zVl^R7ZC&*3X|$a;C~MuTN;)6G@7WrC(-HqxY0cCCn(%0{P$mz=y05{HeIrB^sssJ; zqJbxm_|EHXSc?|5O8@79uz5mpR9baw-7VhMpT>XV7SyN&2U1!OsU_Dnux!K>eC2!o zQ7mvno!OYHc7|*T@+z}mIJK)&>#t@5OAGWz^l-P-UtHo0jc4l<$Gq{*8JCX-9e#hv z=eDHl<4eDJ{SjARgiZ?j^Fsd`TlqXUoyxn*o{|=DV-boe0^|t)dSs#NHEi1Sw~^^~ z_PBYS{j+*ozRgo)fT#s0k0E+l-m(zkeZojMWRaq5jc;L8z!wfl5}#!k%d#Sei^DUw zH1v-M$^N|YD}1FFlRu2OlB)ML7qk-&oHQRbFPyA;fA}Vh*(lm0_>lm=&9qZA;~msAQ5!w&ZlP1w`1Xw{fyr@Z zuTPA3-DTeS*u8#bMT=v=trUptNq2}a;0t}bbe_x#SE}09WB6jo#yU93am*M>^8cg}yY zYmr4(#O;@%PRB5P#~e_A0FeOp94;{)vw#KxNzzXCo&x!nof&8 zcZoKvT4*2i>j=dIOn4hqA;(RE#^g)wsccOoFk&xQJfs4TFk`aplXm946K)(&9ca6G z0%Qa!Aa@$E6uINDwoS2Lz3vt3B@#>+U-I;hDllIWr6&s!g}@7z202?4p(2#$NOVdO zr!UNjygm>HaF%hK(H}LfHc_|S=cQfpga5ocKlrb$qB~4vv2Nlko}RAkGjnY%*4nk6 z7lqdv7VrgNep>CqVmW2#2cb+VL!dj`7A4UO1xgeH`%7!t#@+9|y|P36I&9^W`?MoJ zaO%sC`sUQRT;R!7%8#D@_ygK3Q}fsy8JHHdGRSpG`*{a+T?~+Jb@&f(y`q8@o+`}l zb|C6F&TwxS&L@T_bNeNEqpc5?zAS5Yd*`zfHFTLWOq)hE{hD<5`6yN3M;RQUdNSKs zPM>n!>OFE_)@1y?y5Er9lvJ|umQNalb&0H4ZdxpP;aoQS4NBW5Q*B)93$#azUF#2m z37gDR*MNnA}6sfc@b)3#wm;=EG)jDg z?H$y|#CWxMbsX265^J!GvRP!Fxiw{##`gg9<$>{n9-8!_S8KS9~j>UOj@-2 zp3Wka40!Ox`v8bR#miJMc~n72A>G`9Jlia-Ra;Pv?32(1=`s;Aio|RPE80>Sod(bo zlzIMS$k!}CFURXE=6q|Wfj`0c&8F!~Vv0>KeQB>A@#n)>;&g`2sE&Sy}et6++@6hE&bTULe;Q3L*$Arn4w35`h*GpeSH#dPhjk^Gj}Jea3OyI=-l zxg3tEZ!jD6QTwwPb5ii_cz_s%XXd&9V$y78ZKuPZJ0QSDts@c=RlT_09oZZ>+tR0d za*@$y4O&zZv%{cc!*Fc%?unEA86|aJvT-^*dGX0lKL)`_eonydiyEDVQsIQjd+_dG zhCNY}qztNu#by4RN9fm&$4w?Z<$nhG8<`un0f~mBzOnDHp3-7_(P|Qh)Z4JyG_$3z z14qn}2$kk?f{|oN?K9tO4h6(XeV=#6^p|?=&ZGzTPmqjyza)^jh$_c1ii5F{<_+%d5c_N-)jVVKdF=-!8J;%QV%_6WX zyL5?tS3J5YwIK$ZNw4O#;dciif)e>nCWK)0GIRCCFvDSy{K(#}4^f?3+0smRn$VA; zjt!Bwg$VuerTD~4k<#w3=af{o&M1d@&R<2?;fqvGlElb9Yh?R>sDirn@Y)_$2wj?w zYph(sJ#McZLr6Q@AN;+sb5-Bc0pJuZ5{{u_V`ATns|>_uC*N7hVs_xAae7u!O9E2uw7BkKnj8`a$eiINAJr(r<80%{~~d z{hlC6c`ERT$bXG0{2(#aGezKY+i167yktMm)9Tq(4?Y7U&2JOz~8_2$1zJI(H@S5E;^VW8Fnpf;a zLW6Tvv25EFx31so*UvYdZRW2pR2IQfk2)4sdqz{ggbKxtX)B5ksjl|mB-SCs#)9mmUK4s=Y9GNG|ek+u;Z6exBe7gi@G4c?k*AxG> z#2$A(b6xNO_!>)j_VDnW_HBuLdh6t%H5#UlPM)H{JAfHy>{GqgVNN}c4==VM&n0TP z;Z}PDT9wrk*`Hl+{k`kD27TLnPVBKr2~^}?9r2KEwWJI(gB)iO4rY}k7IlPpTVIT` z7bF?p91_S<(pepZv+YxeKxU%R-^q!AzEqlpl9YHfLgr*j{FyN=}WX+v9-^1oyJ z>gyA!C=KA8Pe!DSZb%Z;!<~|e!tHugx*(dJqz(E34j+#6&+!X|w}qMq+au$R{yIkh zCW`JPim-7MmiO5MT%6G7Yu_6>++7c8w47x`Z?QawbSVCo!W)z;gFkOq9yOwq?)AO5 zdYE{?pM#rmeNSjkgCJDr zM7t9CT|M4WTmb~W2bwcnBqr}I19_CyKPQ`~rdP+05nYG$nYo79+Z4tO59(+={}~nh`WmCo>F5}j?s3z3)NZuEwccopf`k*J&3QT z85@4E@`4K1w9;z=5Ada`lF>5P0pM*NaNNeoBR#O(r&KMjBrVBomqJv_4O?72>f=}x zSwY9O)l_+WLKA^kIfDvQi~zzKXBQX!d7D9jcFZ~gfWg4HGZTY9GN~J0p&eir`g|90 z+bg-`W(8tCf} z7R8e*`SV-JDwZhaZe&p!T2h*G!dmE64NzYeeR0LU2K|J;-}m6R3e$t-oe%cnz?>2u zf4nFw2H!R!=Tbre5oQ=vjBh|V7zAwB!9T#~OgF-Tvs;x@i9hYDf|F*qzwUN@F;4dy zeYo8AEd%#GC!4W}6w^+9_Sx#~8f8lsws&E!M{cvZc&Zqbo}qw{(#RuNlQZY5_eEsIY-I>I`jqIKx{-8A_Tp4uhU2c5emZf8E5!{< zLwukgP3~mM`K)l}1HsbA9^qq)8ExX%O`~eG;qbdaDyGk(`7Pb7C?InMs=+M0RXCS> z5~>e$URGTe*^B92Euv@?@AE4xtnsPrUS4K#{#j!dyvnlk*{}UKj!adCpD;2D_DcA& zvs|Co@&5J0o=o{nkW#DP?c)d;9~@-zOH>ORB#53nEd~py6<@U>mp5wE&MlcY(9CTj z1JG_XxtnQ=q0le!pl@GdfC7Ef@+bQYWPZy*0wq;{5@Moh-~--p159@j;-}B zbn-X5#I?jOJ-ii|P=2P#)I-oL#asM!io5}mBe^^Um}`!5y5>juGF6rAh$t-}t>)dC zPsL`kn5B(ML6I%h%wzA@F|M$3aL!2{@n`B`TK{iWTJqD}kchlE{6+q@e*Klo|1I6ikOr(B<1|9-AV;U6T? zXM89w+_xGYc>q@)%dd|(x6`VLQ#hmA+-S8+5!Se?2uBfwO%*Hd92j^T%L02f#Aehm z=$uHRB^APMbizE{BDI$X`0+;1;Gv479cCdt1 zV%lS+4V**1*5cpf!#tC9Q~XzmZE1>58BOh#bp)&NhioDRaw*&uYI51s=Bw$VjU=BI zn!C>E;9`Isy3T1A^PLmZCp@{>FpBus)Wum>|M;wN>T%q0CJC%jfqY{Q%9_g}2RJ9< zIn5iEoR+@E22Wk29GYpdA{$-%U38=`zHoSikVX@KyhdXm^=LwnwhtR*HeEFO}nfF%F zJM=sDFsVGAPFr)5*6k1K^LbW<8K2qbIAY-!-_nFLl_7gF_(t4N*b`!)Lwxj#dMQ~M zf1z6X~KZ$o;LRL$_{|l$1begmR9_mGcHYGbYSkI z7@lesZ~lYBKSfxWe-Qs z)|bj4PT9o@0ikhZutU|3u!On@_HTwet!(1wg@)Z8hnXhcHMtPN8n7xI2QUjE9Y$NVmH%l&^lKF~@ctI0YCB_4qxIB^KSlVtAB$TOB?0nkIx2#?FAmgtZ8gx> zwuPsQ4f&FYP9hXpx+79_4!zAOohfUPg3`4+NvR&v0kFs}B0~!n)s-pN@<26E5z@EF z&^Ng=m6)N%(ceonPDxU;#u&)R_A7>Wu&W;b9M$QJbbPILa5nixd=Ew8eYWn=D zDvzXdqs1-@Vqv|k*&+2YF+EWf`r>Yk6zASJ0#Yz(BPg*`J9?EIDy9`Jhw^C>hl$n; zsEh1<6JtlpyxY77!1~F?D^{2q;ABs9zFr-bA+Vkkt@F{F2^cdVFd5HasYpwn;4qLE z<-#86!Pu3q_vN=G9QL0Sa*{l6*C}!#I5>0jyONXs$t=(bR1m-ReL4?5ZZ+92(^&lR z4)37om(^j%LtD>kh{{Zb;f(E4kwQoTxE6_3XopH2AXtOxX%B}(*OZ3{C}$Tdzp}4v z=Dk~wcm=1wy7-Qjw&bl{yb(n7van2w)xoDpDte<8bY)AO?6sW4-hMP*^U{<=LEzjZ z)pJRr^K?0J#OKJYj^H_hZ^&c~(tH(Qw(Qm?ah?TdJ0ITMwrP{Hcxg@|w2<1n6i5M6 zc%h}Yz#H}uo`n|G$rf(39)?f3W;#wc-KOvg+ye|E5J4bO;n8sEm>opRmcZc%4{}J* zz8d1oRSwO+_IZusI0BGsH6n4pLHk!&-voXqiXo1hfw~p?C2Y8lJg2^DM%~rMPVqe8OlVDPgC%XS(jjd`d!rx+A5*` zU*BWfE?)(TC(_=)l2?(mIn*4a`tH*MT*;)q*YSdSJpvQskAQbd3VDjaEhTx8IOrQ6 zOd!&A8VQq2W$PYpk5y7O=C?XE2-0_kNC1}HzD=| zk>9~(kNI0a2VA{*6`si7$c^8c%aYSO+#w3wjk;!-9o5rE}j1EdY=`B|*)EEZ~<8&Ip_Qp9qg-(X9<9A=w+)x6IG<*w60a%yJ{6}ir zqtM))&4vePB&@fl5@T(S&m1W?li`q^=AD3q#DH=qk@o86x*JHTd9bhTpQ~^|1;?kk zfWY@JB594~790#Mwxbt3PkFfX>))jm+XP~byg(VBQ7>;}S5_4xCM|nm8JnmR%EFOv zcLQXtQi{8dCAu#dJ{8#mmHI4Sm-f!oo@Y`8MrysEl)*3iI5^BlZehm4bBWj3a$2pQ zkMXQ}Q!n9<8;bMhbGg3M8T?|`dSa&UaAYx~{9#n!fq_$9<&9xrw>6>t7m;R?Oabkn z&b8aH*#`^k+9KF@bN~2_-refz-sBp>KH4L>pEcgnkc+)>Z7Rv)s@GbG1NQV*8xMRj z?-;WLRNFgOslV==P}J=^my3Y^ln>qD>pMRUy_4^Yb7+Ym7+ss5$GEn5WY_Qh9RHBk zSPF5v`Z%#hYh4hCPakeX%7wv!_pOthj%t~JR6Swv$EZHMwpVOyLgR7p^^cJz^Tn&( z9KGZb;&LXY;}uvmrgK!|z(7<7OI4W}iw)s)SNj8~=`Ma+DtRPD@ODsvskZy6wubl- zkwB%lq5e|ZZUC*-y|VsjEpn!rHL&%Bp}jC)DQt@s#AsNh8ibc}FxhX3&;4Odab0xF z6d`obE{$Wf_yA8N0r0)JMYs?}FMjQABAop(V~hFE9tAt>xp=4K-;M3i*|eL7asD5^ zhI43TPlYqLcp-ndn=OGwHWIURW;wHqL!J1;53e)MBk_M`R;HMemw~RD zF1m2gOQeIJHi+uUi+uwlM$&E+uqbz#Eql*%p8p~oew7u8X28eHc#{K&JbV9B z@y~X(V}ZBQQT~3V;SH4LMiayYCZ9BOqIZS{AZ96r9a_ar?qmfALZ!q%1RG_3@3Z3?>21UIXv~dH zTuXnq2Fe;wp%s;93s32YNEB;cW&UgOo&;YX-p6fUA9FZg!BIw=6NM0F#IN#*moI-? zzc&0_Q7leJ74Yl3dr}QX53Q`C#I}73fLJuV3|9%WS{@O84Fs(O7=C#@nd72>Hl;=O zubCdRS9%zqZ@EE^LrHGuYTEb8KDXr=3V$zmF`;7;Hv&2Q${}&NKH`t2T4hXUGJ~Hg zeJ`>Aw7omp1RO9osSU*e2?8lh3;QL#LrG_`?tIqhuZGAt>=h=RJxTLDUk7s6TRo;E z&X*YewGTXGxLKn>25aWFTax4j&L*rk>tD+avcI>?*7+UbM*}Qz@1)N?c`Y|Dl8m32 z1IyxUg+)Iy5GeoLq#EcIDgD?*z~aOKDNjHP3v*JySr%QBN!(D#PuCnxJR8b?m9r`5 zvKdOicL7yuB~Y@Cp*Pr9qX^^gji5LIP)fX#OZ@1%Hj!Wfg0XtUG`r#RsQ;4G%w8q&yDb92> znhu!yC+pUu6Q{yG*IFa`Tqj(Pyy?67thR4WQduAnSI--E7%d0Z54G(Kqco(SCFsmeu#dhGal*_8XMig3x16h5uO*?yQAjW&!lyB_i595 z4IjUB>zEJmg%ye_P#|q97Q=CBlju|k_Q@XwQtQB015%MUk!}-mhEUliN?aukWebt4 zn<69Ul2L%5dWWPS#NFW-R$O-iT6-@P4zdWZ5d=yDaAJzyOs(X;9OD*VNEe^si>UHO zRQnuOaYkg@x+~&!oQAh>y%#P$`D;asHu%}$+6TfNuI|UemCEZ@(}4Q(VAPD|N@D!t zq3mE>%TMB6H#E6ajSb*Y{Frc#98FdScvlvO0dYf<>oY48Cp>XboqD3)E*?(M&(WwNK7kjHW<|B1u zDdX9kDyKCcfJ{)yex-Za#>JXTS1eAQ*=$i|Nl8eLa#>4@))@C_INdkFy_$7I)G)hT z1e?NM0-v)c93YEo%#T|wgFo)pG+bZd2=J42JN}Uc??+tldxE*=);~ElE_>cLtSD3* zud^Wy@I0a2+*SR&9q+r*2S+LGkQFLN8c#-5fAkh4jB1@k{ZQv)_4}I$0l2@Rk^v&e zBrQV7PQy4_xJzD&>89OuMVfJVHSgqAE53Hhslodq^mE&vPhUW77sS4=6 z-WwbKYKVX}!XF%Ysio_u{_Gg#4dAgNHDybi-+*h95mW??$>idtgmA79 zNhl4Dl7>b@fuO`hf=ie_DW`u`A?6E;F_0kYXetk-w;SfX1hxHaJ;~B#1967?_CUbh z*N?v0eNV$ufvGd_&|TgQ$&f_zxevSa_k@R@VJ~t@+;C7|VqTZgr?7m)b`zvL*`MwCH#sFOHS5;4X zSno}fdtQ(k6Z7#st9ZW&TKacA>0C_3!h#gRQY>*q zn7e_Gp#K!;U=DpfkKSeN{)qd$QQuQ($PHm!OXS&ls8Qe=5eun67I=(GsY$F|a9K5D znyJL-@IgWOJd(}oh<+FZB$HQ!YL&vGXEr`XQk8TGu^~3RlF-O6*vYF=dc&TApP@_5iZ9i14-@!*o4_Ut@>@M~56lf@oxp&qwfR%tBVK*Rsj z-eM6@6`4yHT}T{yPlrG=a4?0cKbh&kK;1x~F!2o*eYry+QL1je{2`}UjWZ(Q>uEg& z9Fxn%e_E3gGBj*|9>YiY{Qi%{hC2h`z*i(+#}PFaQm@6}@UrHwgz}`H3n<5Fs`C{g zTNq-fZiW%c&{B!8X8agZiVMG9w=0e=DQQ6U;Gv)1tdU4eZwspw)l}2eWPYeMa-57n zFnzae#FvdBqlt{pvQdQLMZ=Sp=_7WobS{?!IjLt6=7hjSIHq!XvZ7OdJt(=5p892%8H|Y{stzK_7lq?z>izA=!}gv$azF86D$tOaNjq7{PSJovlYto)>RPT zTc;J}`2}9S+*3OLrNi{lWyQQ4q;2uRGvlU4FN;lgT+c$3D0L~sWv+W_oG&8k#eI5w zI2mo5@v#yp7>GJX6Lfq}^PTQQ4pM*`2xyo+xT)K+GrBv$GM5(&a#SE~S6K2rbhc}K zWbob#vD^9g$>`ah{OArv8=)Vub^v^Stil;Lxor zem$erD5RcaMEd!pv4iO0TMa8iuHSDcx?T0Gja+tFC$T2JVyV2guljTiE=e`oS46se zfruH8&?G>w;=~;Pf+H?6T6wR-sy2o*Z?%@1MLW6uDhA=mtt;r}Xr7Z#Nc1^ZZsLZQ z4N}M8W>VgYwDsC<{^aL(J!g_&){vlWQc1?Nw!Gxd+LwYha8Qy6J%S~OxT66;#UZto zk3MwJ)7?)5^5|3dx>-7Jy+4vM;8Z&~<$L$TLW$XN~7e(4W z`q*w&uGdogyy0(nXu3%vptuEZlS!`?8#7(k#YwZAZdn+}R(?ZC=46nlXcI}-YZZOT zb>ue>d(M+NBd+#ipTV|&tGfN&I#nLwSNN3IanBWb8Ab8r*w5DB!NT)01HZjo0Si34 zd_?lsLeDn}!GG(00r$I4(hWpsd%>Rf{0#E{N!<{9 literal 0 HcmV?d00001 diff --git a/WindowsInstaller/Qortal.aip b/WindowsInstaller/Qortal.aip index 1b0c944b..a3dbd88a 100755 --- a/WindowsInstaller/Qortal.aip +++ b/WindowsInstaller/Qortal.aip @@ -4,10 +4,12 @@ - - + + + + @@ -16,19 +18,22 @@ - + + + + + - + - + - - + @@ -40,6 +45,7 @@ + @@ -133,232 +139,239 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + @@ -368,497 +381,505 @@ - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -869,8 +890,11 @@ + + + - + @@ -899,19 +923,19 @@ - - - - - + + + + + + - + - - - + + @@ -941,175 +965,76 @@ + + + + + + + + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -1129,6 +1054,7 @@ + @@ -1145,6 +1071,8 @@ + + @@ -1162,6 +1090,7 @@ + @@ -1180,11 +1109,6 @@ - - - - - @@ -1195,15 +1119,20 @@ + + + + + @@ -1217,7 +1146,7 @@ - + @@ -1239,6 +1168,7 @@ + @@ -1279,9 +1209,15 @@ - + + + + + + + @@ -1297,13 +1233,6 @@ - - - - - - - @@ -1327,86 +1256,87 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1428,12 +1358,12 @@ - + - + @@ -1462,6 +1392,8 @@ + + @@ -1469,7 +1401,7 @@ - + @@ -1477,6 +1409,8 @@ + + @@ -1491,15 +1425,13 @@ - - - - - - + + + + @@ -1508,6 +1440,7 @@ + @@ -1559,8 +1492,11 @@ + + + - + diff --git a/WindowsInstaller/README.md b/WindowsInstaller/README.md index 29a7b64a..4431564e 100644 --- a/WindowsInstaller/README.md +++ b/WindowsInstaller/README.md @@ -2,7 +2,9 @@ ## Prerequisites -* AdvancedInstaller v19.4 or better, and enterprise licence if translations are required +* AdvancedInstaller v19.4 or better, and enterprise licence. +* Qortal has an open source license, however it currently (as of December 2024) only supports up to version 19. (We may need to reach out to Advanced Installer again to obtain a new license at some point, if needed. +* Reach out to @crowetic for links to the installer install files, and license. * Installed AdoptOpenJDK v17 64bit, full JDK *not* JRE ## General build instructions @@ -10,6 +12,12 @@ If this is your first time opening the `qortal.aip` file then you might need to adjust configured paths, or create a dummy `D:` drive with the expected layout. +Opening the aip file from within a clone of the qortal repo also works, if you have a separate windows machine setup to do the build. + +You May need to change the location of the 'jre64' files inside Advanced Installer, if it is set to a path that your build machine doesn't have. + +The Java Memory Arguments can be set manually, but as of December 2024 they have been reset back to system defaults. This should include G1GC Garbage Collector. + Typical build procedure: * Place the `qortal.jar` file in `Install-Files\`