From 914cd004612bdc9ec5393c0f3810ff33d1191913 Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Thu, 7 Nov 2024 22:55:26 -0400 Subject: [PATCH 01/23] Add serializeCompactV2 method to serialize 32 byte chain work --- .../java/org/bitcoinj/core/StoredBlock.java | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/StoredBlock.java b/core/src/main/java/org/bitcoinj/core/StoredBlock.java index ed771c63f3d..eeb46665e7c 100644 --- a/core/src/main/java/org/bitcoinj/core/StoredBlock.java +++ b/core/src/main/java/org/bitcoinj/core/StoredBlock.java @@ -35,11 +35,19 @@ */ public class StoredBlock { - // A BigInteger representing the total amount of work done so far on this chain. As of May 2011 it takes 8 - // bytes to represent this field, so 12 bytes should be plenty for now. - public static final int CHAIN_WORK_BYTES = 12; - public static final byte[] EMPTY_BYTES = new byte[CHAIN_WORK_BYTES]; - public static final int COMPACT_SERIALIZED_SIZE = Block.HEADER_SIZE + CHAIN_WORK_BYTES + 4; // for height + // A BigInteger representing the total amount of work done so far on this chain. As of June 22, 2024, it takes 12 + // unsigned bytes to store this value, so developers should use the V2 format. + private static final int CHAIN_WORK_BYTES_V1 = 12; + // A BigInteger representing the total amount of work done so far on this chain. + private static final int CHAIN_WORK_BYTES_V2 = 32; + // Height is an int. + private static final int HEIGHT_BYTES = 4; + // Used for padding. + private static final byte[] EMPTY_BYTES = new byte[CHAIN_WORK_BYTES_V2]; // fit larger format + /** Number of bytes serialized by {@link #serializeCompact(ByteBuffer)} */ + public static final int COMPACT_SERIALIZED_SIZE = Block.HEADER_SIZE + CHAIN_WORK_BYTES_V1 + HEIGHT_BYTES; + /** Number of bytes serialized by {@link #serializeCompactV2(ByteBuffer)} */ + public static final int COMPACT_SERIALIZED_SIZE_V2 = Block.HEADER_SIZE + CHAIN_WORK_BYTES_V2 + HEIGHT_BYTES; private Block header; private BigInteger chainWork; @@ -113,12 +121,37 @@ public StoredBlock getPrev(BlockStore store) throws BlockStoreException { return store.get(getHeader().getPrevBlockHash()); } - /** Serializes the stored block to a custom packed format. Used by {@link CheckpointManager}. */ + /** + * Serializes the stored block to a custom packed format. Used internally. + * As of June 22, 2024, it takes 12 unsigned bytes to store the chain work value, + * so developers should use the V2 format. + * + * @param buffer buffer to write to + */ public void serializeCompact(ByteBuffer buffer) { - byte[] chainWorkBytes = Utils.bigIntegerToBytes(getChainWork(), CHAIN_WORK_BYTES); - if (chainWorkBytes.length < CHAIN_WORK_BYTES) { + byte[] chainWorkBytes = Utils.bigIntegerToBytes(getChainWork(), CHAIN_WORK_BYTES_V1); + if (chainWorkBytes.length < CHAIN_WORK_BYTES_V1) { + // Pad to the right size. + buffer.put(EMPTY_BYTES, 0, CHAIN_WORK_BYTES_V1 - chainWorkBytes.length); + } + buffer.put(chainWorkBytes); + buffer.putInt(getHeight()); + // Using unsafeBitcoinSerialize here can give us direct access to the same bytes we read off the wire, + // avoiding serialization round-trips. + byte[] bytes = getHeader().unsafeBitcoinSerialize(); + buffer.put(bytes, 0, Block.HEADER_SIZE); // Trim the trailing 00 byte (zero transactions). + } + + /** + * Serializes the stored block to a custom packed format. Used internally. + * + * @param buffer buffer to write to + */ + public void serializeCompactV2(ByteBuffer buffer) { + byte[] chainWorkBytes = Utils.bigIntegerToBytes(getChainWork(), CHAIN_WORK_BYTES_V2); + if (chainWorkBytes.length < CHAIN_WORK_BYTES_V2) { // Pad to the right size. - buffer.put(EMPTY_BYTES, 0, CHAIN_WORK_BYTES - chainWorkBytes.length); + buffer.put(EMPTY_BYTES, 0, CHAIN_WORK_BYTES_V2 - chainWorkBytes.length); } buffer.put(chainWorkBytes); buffer.putInt(getHeight()); @@ -130,7 +163,7 @@ public void serializeCompact(ByteBuffer buffer) { /** De-serializes the stored block from a custom packed format. Used by {@link CheckpointManager}. */ public static StoredBlock deserializeCompact(NetworkParameters params, ByteBuffer buffer) throws ProtocolException { - byte[] chainWorkBytes = new byte[StoredBlock.CHAIN_WORK_BYTES]; + byte[] chainWorkBytes = new byte[StoredBlock.CHAIN_WORK_BYTES_V1]; buffer.get(chainWorkBytes); BigInteger chainWork = new BigInteger(1, chainWorkBytes); int height = buffer.getInt(); // +4 bytes From 8d0138ad76a0cf651a420a01f2753fab19d12875 Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Thu, 7 Nov 2024 23:00:44 -0400 Subject: [PATCH 02/23] Add tests for serializeCompactV2 --- .../org/bitcoinj/core/StoredBlockTest.java | 130 ++++++++++++++++-- 1 file changed, 116 insertions(+), 14 deletions(-) diff --git a/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java b/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java index c743aab4081..7dce801b846 100644 --- a/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java +++ b/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java @@ -1,24 +1,47 @@ package org.bitcoinj.core; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import java.math.BigInteger; import java.nio.ByteBuffer; import org.bouncycastle.util.encoders.Hex; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; public class StoredBlockTest { + private static final BigInteger NEGATIVE_CHAIN_WORK = BigInteger.valueOf(-1); + private static final BigInteger ZERO_CHAIN_WORK = BigInteger.ZERO; + private static final BigInteger SMALL_CHAIN_WORK = BigInteger.ONE; + // 8 bytes chain work + private static final BigInteger EIGHT_BYTES_WORK_V1 = new BigInteger("ffffffffffffffff", 16); // 8 bytes + // Max chain work to fit in 12 bytes + private static final BigInteger MAX_WORK_V1 = new BigInteger(/* 12 bytes */ "ffffffffffffffffffffffff", 16); + // Chain work too large to fit in 12 bytes + private static final BigInteger TOO_LARGE_WORK_V1 = new BigInteger(/* 13 bytes */ "ffffffffffffffffffffffffff", 16); + // Max chain work to fit in 32 bytes + private static final BigInteger MAX_WORK_V2 = new BigInteger(/* 32 bytes */ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16); + // Chain work too large to fit in 32 bytes + private static final BigInteger TOO_LARGE_WORK_V2 = new BigInteger(/* 33 bytes */ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16); + private static final NetworkParameters mainnet = NetworkParameters.fromID(NetworkParameters.ID_MAINNET); + private static final BitcoinSerializer bitcoinSerializer = new BitcoinSerializer(mainnet, false); // Just an arbitrary block private static final String blockHeader = "00e00820925b77c9ff4d0036aa29f3238cde12e9af9d55c34ed30200000000000000000032a9fa3e12ef87a2327b55db6a16a1227bb381db8b269d90aa3a6e38cf39665f91b47766255d0317c1b1575f"; private static final int blockHeight = 849137; - private static final Block block = new Block(mainnet, Hex.decode(blockHeader)); + private static final Block block = bitcoinSerializer.makeBlock(Hex.decode(blockHeader)); private static final int blockCapacity = StoredBlock.COMPACT_SERIALIZED_SIZE; private ByteBuffer blockBuffer; + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + @Before public void setUp() { blockBuffer = ByteBuffer.allocate(blockCapacity); @@ -26,19 +49,26 @@ public void setUp() { @Test public void newStoredBlock_createsExpectedBlock() { - BigInteger chainWork = new BigInteger("ffffffffffffffff", 16); // 8 bytes - StoredBlock blockToStore = new StoredBlock(block, chainWork, blockHeight); + StoredBlock blockToStore = new StoredBlock(block, EIGHT_BYTES_WORK_V1, blockHeight); // assert block was correctly created - assertEquals(chainWork, blockToStore.getChainWork()); + assertEquals(EIGHT_BYTES_WORK_V1, blockToStore.getChainWork()); assertEquals(block, blockToStore.getHeader()); assertEquals(blockHeight, blockToStore.getHeight()); } + @Test(expected = IllegalArgumentException.class) + public void serializeCompact_forNegativeChainWork_throwsException() { + + StoredBlock blockToStore = new StoredBlock(block, NEGATIVE_CHAIN_WORK, blockHeight); + + // serialize block should throw illegal argument exception + blockToStore.serializeCompact(blockBuffer); + } + @Test public void serializeAndDeserializeCompact_forZeroChainWork_works() { - BigInteger chainWork = BigInteger.ZERO; - StoredBlock blockToStore = new StoredBlock(block, chainWork, blockHeight); + StoredBlock blockToStore = new StoredBlock(block, ZERO_CHAIN_WORK, blockHeight); // serialize block blockToStore.serializeCompact(blockBuffer); @@ -52,8 +82,7 @@ public void serializeAndDeserializeCompact_forZeroChainWork_works() { @Test public void serializeAndDeserializeCompact_forSmallChainWork_works() { - BigInteger chainWork = BigInteger.ONE; - StoredBlock blockToStore = new StoredBlock(block, chainWork, blockHeight); + StoredBlock blockToStore = new StoredBlock(block, SMALL_CHAIN_WORK, blockHeight); // serialize block blockToStore.serializeCompact(blockBuffer); @@ -67,8 +96,7 @@ public void serializeAndDeserializeCompact_forSmallChainWork_works() { @Test public void serializeAndDeserializeCompact_for8bytesChainWork_works() { - BigInteger chainWork = new BigInteger("ffffffffffffffff", 16); // 8 bytes - StoredBlock blockToStore = new StoredBlock(block, chainWork, blockHeight); + StoredBlock blockToStore = new StoredBlock(block, EIGHT_BYTES_WORK_V1, blockHeight); // serialize block blockToStore.serializeCompact(blockBuffer); @@ -82,8 +110,7 @@ public void serializeAndDeserializeCompact_for8bytesChainWork_works() { @Test public void serializeAndDeserializeCompact_forMax12bytesChainWork_works() { - BigInteger chainWork = new BigInteger("ffffffffffffffffffffffff", 16); // max chain work to fit in 12 unsigned bytes - StoredBlock blockToStore = new StoredBlock(block, chainWork, blockHeight); + StoredBlock blockToStore = new StoredBlock(block, MAX_WORK_V1, blockHeight); // serialize block blockToStore.serializeCompact(blockBuffer); @@ -97,10 +124,85 @@ public void serializeAndDeserializeCompact_forMax12bytesChainWork_works() { @Test(expected = IllegalArgumentException.class) public void serializeCompact_for13bytesChainWork_throwsException() { - BigInteger chainWork = new BigInteger("ffffffffffffffffffffffff", 16).add(BigInteger.valueOf(1)); // too large chain work to fit in 12 unsigned bytes - StoredBlock blockToStore = new StoredBlock(block, chainWork, blockHeight); + StoredBlock blockToStore = new StoredBlock(block, TOO_LARGE_WORK_V1, blockHeight); // serialize block should throw illegal argument exception blockToStore.serializeCompact(blockBuffer); } + + @Test(expected = IllegalArgumentException.class) + public void serializeCompact_forMoreThan32bytesChainWork_throwsException() { + StoredBlock blockToStore = new StoredBlock(block, TOO_LARGE_WORK_V2, blockHeight); + + // serialize block should throw illegal argument exception + blockToStore.serializeCompact(blockBuffer); + } + + + @Test(expected = IllegalArgumentException.class) + public void serializeCompactV2_forNegativeChainWork_throwsException() { + StoredBlock blockToStore = new StoredBlock(block, NEGATIVE_CHAIN_WORK, blockHeight); + + // serialize block should throw illegal argument exception + blockToStore.serializeCompactV2(blockBuffer); + } + + @Test(expected = IllegalArgumentException.class) + public void serializeCompactV2_forMoreThan32bytesChainWork_throwsException() { + StoredBlock blockToStore = new StoredBlock(block, TOO_LARGE_WORK_V2, blockHeight); + + // serialize block should throw illegal argument exception + blockToStore.serializeCompactV2(blockBuffer); + } + + @Test + public void serializeCompactV2_forZeroChainWork_works() { + testSerializeCompactV2(ZERO_CHAIN_WORK); + } + + @Test + public void serializeCompactV2_forSmallChainWork_works() { + testSerializeCompactV2(SMALL_CHAIN_WORK); + } + + @Test + public void serializeCompactV2_for8BytesChainWork_works() { + testSerializeCompactV2(EIGHT_BYTES_WORK_V1); + } + + @Test + public void serializeCompactV2_for12ByteChainWork_works() { + testSerializeCompactV2(MAX_WORK_V1); + } + + @Test + public void serializeCompactV2_forTooLargeWorkV1_works() { + testSerializeCompactV2(TOO_LARGE_WORK_V1); + } + + @Test + public void serializeCompactV2_for32BytesChainWork_works() { + testSerializeCompactV2(MAX_WORK_V2); + } + + private void testSerializeCompactV2(BigInteger chainWork) { + StoredBlock blockToStore = new StoredBlock(block, chainWork, 0); + ByteBuffer buf = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_V2); + blockToStore.serializeCompactV2(buf); + // assert serialized size and that the buffer is full + assertEquals(StoredBlock.COMPACT_SERIALIZED_SIZE_V2, buf.position()); + } + + @Test + public void moreWorkThan_whenLargerVsSmallerChainWork_shouldReturnTrue() { + StoredBlock noWorkBlock = new StoredBlock(block, BigInteger.ZERO, 0); + StoredBlock smallWorkBlock = new StoredBlock(block, BigInteger.ONE, 0); + StoredBlock maxWorkBlockV1 = new StoredBlock(block, MAX_WORK_V1, 0); + StoredBlock maxWorkBlockV2 = new StoredBlock(block, MAX_WORK_V2, 0); + + assertTrue(smallWorkBlock.moreWorkThan(noWorkBlock)); + assertTrue(maxWorkBlockV1.moreWorkThan(noWorkBlock)); + assertTrue(maxWorkBlockV1.moreWorkThan(smallWorkBlock)); + assertTrue(maxWorkBlockV2.moreWorkThan(maxWorkBlockV1)); + } } From 989732a8655adbbc7ce4e8f6055b5698f3be401c Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Mon, 11 Nov 2024 01:04:38 -0400 Subject: [PATCH 03/23] - Improve comments - Reorganize statics - Remove unrelated tests --- core/src/main/java/org/bitcoinj/core/StoredBlock.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/StoredBlock.java b/core/src/main/java/org/bitcoinj/core/StoredBlock.java index eeb46665e7c..28887424ee2 100644 --- a/core/src/main/java/org/bitcoinj/core/StoredBlock.java +++ b/core/src/main/java/org/bitcoinj/core/StoredBlock.java @@ -35,8 +35,8 @@ */ public class StoredBlock { - // A BigInteger representing the total amount of work done so far on this chain. As of June 22, 2024, it takes 12 - // unsigned bytes to store this value, so developers should use the V2 format. + /* A BigInteger representing the total amount of work done so far on this chain. As of June 22, 2024, it takes 12 + unsigned bytes to store this value, so developers should use the V2 format. */ private static final int CHAIN_WORK_BYTES_V1 = 12; // A BigInteger representing the total amount of work done so far on this chain. private static final int CHAIN_WORK_BYTES_V2 = 32; @@ -44,9 +44,9 @@ public class StoredBlock { private static final int HEIGHT_BYTES = 4; // Used for padding. private static final byte[] EMPTY_BYTES = new byte[CHAIN_WORK_BYTES_V2]; // fit larger format - /** Number of bytes serialized by {@link #serializeCompact(ByteBuffer)} */ + // Number of bytes serialized by {@link #serializeCompact(ByteBuffer)} public static final int COMPACT_SERIALIZED_SIZE = Block.HEADER_SIZE + CHAIN_WORK_BYTES_V1 + HEIGHT_BYTES; - /** Number of bytes serialized by {@link #serializeCompactV2(ByteBuffer)} */ + // Number of bytes serialized by {@link #serializeCompactV2(ByteBuffer)} public static final int COMPACT_SERIALIZED_SIZE_V2 = Block.HEADER_SIZE + CHAIN_WORK_BYTES_V2 + HEIGHT_BYTES; private Block header; From 742c51a7d43cd11ff1f650a3a85f5219e804b9b2 Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Tue, 12 Nov 2024 14:31:27 -0400 Subject: [PATCH 04/23] - Improve comments to understand key class variables meaning - Rename serializeCompact to serializeCompactLegacy to avoid confusion - Add deprecate tag to serializeCompactLegacy - Remove unnecessary check of chainWorkBytes size --- .../java/org/bitcoinj/core/StoredBlock.java | 26 +++++++---------- .../org/bitcoinj/store/LevelDBBlockStore.java | 2 +- .../store/LevelDBFullPrunedBlockStore.java | 2 +- .../org/bitcoinj/store/SPVBlockStore.java | 2 +- .../org/bitcoinj/core/StoredBlockTest.java | 28 +++++++++---------- .../org/bitcoinj/tools/BuildCheckpoints.java | 4 +-- 6 files changed, 29 insertions(+), 35 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/StoredBlock.java b/core/src/main/java/org/bitcoinj/core/StoredBlock.java index 28887424ee2..59feee8da0a 100644 --- a/core/src/main/java/org/bitcoinj/core/StoredBlock.java +++ b/core/src/main/java/org/bitcoinj/core/StoredBlock.java @@ -35,18 +35,17 @@ */ public class StoredBlock { - /* A BigInteger representing the total amount of work done so far on this chain. As of June 22, 2024, it takes 12 + /* Size in bytes to represent the total amount of work done so far on this chain. As of June 22, 2024, it takes 12 unsigned bytes to store this value, so developers should use the V2 format. */ private static final int CHAIN_WORK_BYTES_V1 = 12; - // A BigInteger representing the total amount of work done so far on this chain. + // Size in bytes to represent the total amount of work done so far on this chain. private static final int CHAIN_WORK_BYTES_V2 = 32; - // Height is an int. + // Size in bytes(int) to represent btc block height private static final int HEIGHT_BYTES = 4; - // Used for padding. - private static final byte[] EMPTY_BYTES = new byte[CHAIN_WORK_BYTES_V2]; // fit larger format - // Number of bytes serialized by {@link #serializeCompact(ByteBuffer)} + + // Size in bytes of serialized block in legacy format by {@link #serializeCompactLegacy(ByteBuffer)} public static final int COMPACT_SERIALIZED_SIZE = Block.HEADER_SIZE + CHAIN_WORK_BYTES_V1 + HEIGHT_BYTES; - // Number of bytes serialized by {@link #serializeCompactV2(ByteBuffer)} + // Size in bytes of serialized block in V2 format by {@link #serializeCompactV2(ByteBuffer)} public static final int COMPACT_SERIALIZED_SIZE_V2 = Block.HEADER_SIZE + CHAIN_WORK_BYTES_V2 + HEIGHT_BYTES; private Block header; @@ -122,18 +121,17 @@ public StoredBlock getPrev(BlockStore store) throws BlockStoreException { } /** + * * @deprecated Use {@link #serializeCompactV2(ByteBuffer)} instead. + * * Serializes the stored block to a custom packed format. Used internally. * As of June 22, 2024, it takes 12 unsigned bytes to store the chain work value, * so developers should use the V2 format. * * @param buffer buffer to write to */ - public void serializeCompact(ByteBuffer buffer) { + @Deprecated + public void serializeCompactLegacy(ByteBuffer buffer) { byte[] chainWorkBytes = Utils.bigIntegerToBytes(getChainWork(), CHAIN_WORK_BYTES_V1); - if (chainWorkBytes.length < CHAIN_WORK_BYTES_V1) { - // Pad to the right size. - buffer.put(EMPTY_BYTES, 0, CHAIN_WORK_BYTES_V1 - chainWorkBytes.length); - } buffer.put(chainWorkBytes); buffer.putInt(getHeight()); // Using unsafeBitcoinSerialize here can give us direct access to the same bytes we read off the wire, @@ -149,10 +147,6 @@ public void serializeCompact(ByteBuffer buffer) { */ public void serializeCompactV2(ByteBuffer buffer) { byte[] chainWorkBytes = Utils.bigIntegerToBytes(getChainWork(), CHAIN_WORK_BYTES_V2); - if (chainWorkBytes.length < CHAIN_WORK_BYTES_V2) { - // Pad to the right size. - buffer.put(EMPTY_BYTES, 0, CHAIN_WORK_BYTES_V2 - chainWorkBytes.length); - } buffer.put(chainWorkBytes); buffer.putInt(getHeight()); // Using unsafeBitcoinSerialize here can give us direct access to the same bytes we read off the wire, diff --git a/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java b/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java index ba3d8255903..febcd157ffe 100644 --- a/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java @@ -79,7 +79,7 @@ private synchronized void initStoreIfNeeded() throws BlockStoreException { @Override public synchronized void put(StoredBlock block) throws BlockStoreException { buffer.clear(); - block.serializeCompact(buffer); + block.serializeCompactLegacy(buffer); db.put(block.getHeader().getHash().getBytes(), buffer.array()); } diff --git a/core/src/main/java/org/bitcoinj/store/LevelDBFullPrunedBlockStore.java b/core/src/main/java/org/bitcoinj/store/LevelDBFullPrunedBlockStore.java index 814f953808d..6f694333713 100644 --- a/core/src/main/java/org/bitcoinj/store/LevelDBFullPrunedBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/LevelDBFullPrunedBlockStore.java @@ -495,7 +495,7 @@ protected void putUpdateStoredBlock(StoredBlock storedBlock, boolean wasUndoable beginMethod("putUpdateStoredBlock"); Sha256Hash hash = storedBlock.getHeader().getHash(); ByteBuffer bb = ByteBuffer.allocate(97); - storedBlock.serializeCompact(bb); + storedBlock.serializeCompactLegacy(bb); bb.put((byte) (wasUndoable ? 1 : 0)); batchPut(getKey(KeyType.HEADERS_ALL, hash), bb.array()); if (instrument) diff --git a/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java b/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java index 298dbaf7482..e49f1db1735 100644 --- a/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java @@ -196,7 +196,7 @@ public void put(StoredBlock block) throws BlockStoreException { Sha256Hash hash = block.getHeader().getHash(); notFoundCache.remove(hash); buffer.put(hash.getBytes()); - block.serializeCompact(buffer); + block.serializeCompactLegacy(buffer); setRingCursor(buffer, buffer.position()); blockCache.put(hash, block); } finally { lock.unlock(); } diff --git a/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java b/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java index 7dce801b846..28b9b903bbb 100644 --- a/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java +++ b/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java @@ -58,20 +58,20 @@ public void newStoredBlock_createsExpectedBlock() { } @Test(expected = IllegalArgumentException.class) - public void serializeCompact_forNegativeChainWork_throwsException() { + public void serializeCompactLegacy_forNegativeChainWork_throwsException() { StoredBlock blockToStore = new StoredBlock(block, NEGATIVE_CHAIN_WORK, blockHeight); // serialize block should throw illegal argument exception - blockToStore.serializeCompact(blockBuffer); + blockToStore.serializeCompactLegacy(blockBuffer); } @Test - public void serializeAndDeserializeCompact_forZeroChainWork_works() { + public void serializeAndDeserializeCompactLegacy_forZeroChainWork_works() { StoredBlock blockToStore = new StoredBlock(block, ZERO_CHAIN_WORK, blockHeight); // serialize block - blockToStore.serializeCompact(blockBuffer); + blockToStore.serializeCompactLegacy(blockBuffer); assertEquals(blockCapacity, blockBuffer.position()); // deserialize block @@ -81,11 +81,11 @@ public void serializeAndDeserializeCompact_forZeroChainWork_works() { } @Test - public void serializeAndDeserializeCompact_forSmallChainWork_works() { + public void serializeAndDeserializeCompactLegacy_forSmallChainWork_works() { StoredBlock blockToStore = new StoredBlock(block, SMALL_CHAIN_WORK, blockHeight); // serialize block - blockToStore.serializeCompact(blockBuffer); + blockToStore.serializeCompactLegacy(blockBuffer); assertEquals(blockCapacity, blockBuffer.position()); // deserialize block @@ -95,11 +95,11 @@ public void serializeAndDeserializeCompact_forSmallChainWork_works() { } @Test - public void serializeAndDeserializeCompact_for8bytesChainWork_works() { + public void serializeAndDeserializeCompactLegacy_for8BytesChainWork_works() { StoredBlock blockToStore = new StoredBlock(block, EIGHT_BYTES_WORK_V1, blockHeight); // serialize block - blockToStore.serializeCompact(blockBuffer); + blockToStore.serializeCompactLegacy(blockBuffer); assertEquals(blockCapacity, blockBuffer.position()); // deserialize block @@ -109,11 +109,11 @@ public void serializeAndDeserializeCompact_for8bytesChainWork_works() { } @Test - public void serializeAndDeserializeCompact_forMax12bytesChainWork_works() { + public void serializeAndDeserializeCompactLegacy_forMax12BytesChainWork_works() { StoredBlock blockToStore = new StoredBlock(block, MAX_WORK_V1, blockHeight); // serialize block - blockToStore.serializeCompact(blockBuffer); + blockToStore.serializeCompactLegacy(blockBuffer); assertEquals(blockCapacity, blockBuffer.position()); // deserialize block @@ -123,19 +123,19 @@ public void serializeAndDeserializeCompact_forMax12bytesChainWork_works() { } @Test(expected = IllegalArgumentException.class) - public void serializeCompact_for13bytesChainWork_throwsException() { + public void serializeCompactLegacy_for13BytesChainWork_throwsException() { StoredBlock blockToStore = new StoredBlock(block, TOO_LARGE_WORK_V1, blockHeight); // serialize block should throw illegal argument exception - blockToStore.serializeCompact(blockBuffer); + blockToStore.serializeCompactLegacy(blockBuffer); } @Test(expected = IllegalArgumentException.class) - public void serializeCompact_forMoreThan32bytesChainWork_throwsException() { + public void serializeCompactLegacy_forMoreThan32BytesChainWork_throwsException() { StoredBlock blockToStore = new StoredBlock(block, TOO_LARGE_WORK_V2, blockHeight); // serialize block should throw illegal argument exception - blockToStore.serializeCompact(blockBuffer); + blockToStore.serializeCompactLegacy(blockBuffer); } diff --git a/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java b/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java index fb8b8d4b29b..4ea5a0bf471 100644 --- a/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java +++ b/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java @@ -183,7 +183,7 @@ private static void writeBinaryCheckpoints(TreeMap checkpo dataOutputStream.writeInt(checkpoints.size()); ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); for (StoredBlock block : checkpoints.values()) { - block.serializeCompact(buffer); + block.serializeCompactLegacy(buffer); dataOutputStream.write(buffer.array()); buffer.position(0); } @@ -202,7 +202,7 @@ private static void writeTextualCheckpoints(TreeMap checkp writer.println(checkpoints.size()); ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); for (StoredBlock block : checkpoints.values()) { - block.serializeCompact(buffer); + block.serializeCompactLegacy(buffer); writer.println(CheckpointManager.BASE64.encode(buffer.array())); buffer.position(0); } From 698a660aebd10963899b02b43df70e047c46aa79 Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Thu, 7 Nov 2024 23:42:01 -0400 Subject: [PATCH 05/23] Add deserializeCompactV2 method to deserialize 32 byte chain work --- .../java/org/bitcoinj/core/StoredBlock.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/bitcoinj/core/StoredBlock.java b/core/src/main/java/org/bitcoinj/core/StoredBlock.java index 59feee8da0a..0be255dcb1b 100644 --- a/core/src/main/java/org/bitcoinj/core/StoredBlock.java +++ b/core/src/main/java/org/bitcoinj/core/StoredBlock.java @@ -155,7 +155,14 @@ public void serializeCompactV2(ByteBuffer buffer) { buffer.put(bytes, 0, Block.HEADER_SIZE); // Trim the trailing 00 byte (zero transactions). } - /** De-serializes the stored block from a custom packed format. Used by {@link CheckpointManager}. */ + /** + * Deserializes the stored block from a custom packed format. Used internally. + * As of June 22, 2024, it takes 12 unsigned bytes to store the chain work value, + * so developers should use the V2 format. + * + * @param buffer data to deserialize + * @return deserialized stored block + */ public static StoredBlock deserializeCompact(NetworkParameters params, ByteBuffer buffer) throws ProtocolException { byte[] chainWorkBytes = new byte[StoredBlock.CHAIN_WORK_BYTES_V1]; buffer.get(chainWorkBytes); @@ -166,6 +173,22 @@ public static StoredBlock deserializeCompact(NetworkParameters params, ByteBuffe return new StoredBlock(params.getDefaultSerializer().makeBlock(header), chainWork, height); } + /** + * Deserializes the stored block from a custom packed format. Used internally. + * + * @param buffer data to deserialize + * @return deserialized stored block + */ + public static StoredBlock deserializeCompactV2(NetworkParameters params, ByteBuffer buffer) throws ProtocolException { + byte[] chainWorkBytes = new byte[StoredBlock.CHAIN_WORK_BYTES_V2]; + buffer.get(chainWorkBytes); + BigInteger chainWork = new BigInteger(1, chainWorkBytes); + int height = buffer.getInt(); // +4 bytes + byte[] header = new byte[Block.HEADER_SIZE + 1]; // Extra byte for the 00 transactions length. + buffer.get(header, 0, Block.HEADER_SIZE); + return new StoredBlock(params.getDefaultSerializer().makeBlock(header), chainWork, height); + } + @Override public String toString() { return String.format(Locale.US, "Block %s at height %d: %s", From 2c347fe304d5cd08e7684e536c9cfe6aacaf1935 Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Thu, 7 Nov 2024 23:44:10 -0400 Subject: [PATCH 06/23] Add tests for deserializeCompactV2 --- .../org/bitcoinj/core/StoredBlockTest.java | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java b/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java index 28b9b903bbb..b4bfe97ac04 100644 --- a/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java +++ b/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java @@ -156,41 +156,45 @@ public void serializeCompactV2_forMoreThan32bytesChainWork_throwsException() { } @Test - public void serializeCompactV2_forZeroChainWork_works() { - testSerializeCompactV2(ZERO_CHAIN_WORK); + public void serializeAndDeserializeCompactV2_forZeroChainWork_works() { + testSerializeAndDeserializeCompactV2(ZERO_CHAIN_WORK); } @Test - public void serializeCompactV2_forSmallChainWork_works() { - testSerializeCompactV2(SMALL_CHAIN_WORK); + public void serializeAndDeserializeCompactV2_forSmallChainWork_works() { + testSerializeAndDeserializeCompactV2(SMALL_CHAIN_WORK); } @Test - public void serializeCompactV2_for8BytesChainWork_works() { - testSerializeCompactV2(EIGHT_BYTES_WORK_V1); + public void serializeAndDeserializeCompactV2_for8BytesChainWork_works() { + testSerializeAndDeserializeCompactV2(EIGHT_BYTES_WORK_V1); } @Test - public void serializeCompactV2_for12ByteChainWork_works() { - testSerializeCompactV2(MAX_WORK_V1); + public void serializeAndDeserializeCompactV2_for12ByteChainWork_works() { + testSerializeAndDeserializeCompactV2(MAX_WORK_V1); } @Test - public void serializeCompactV2_forTooLargeWorkV1_works() { - testSerializeCompactV2(TOO_LARGE_WORK_V1); + public void serializeAndDeserializeCompactV2_forTooLargeWorkV1_works() { + testSerializeAndDeserializeCompactV2(TOO_LARGE_WORK_V1); } @Test - public void serializeCompactV2_for32BytesChainWork_works() { - testSerializeCompactV2(MAX_WORK_V2); + public void serializeAndDeserializeCompactV2_for32BytesChainWork_works() { + testSerializeAndDeserializeCompactV2(MAX_WORK_V2); } - private void testSerializeCompactV2(BigInteger chainWork) { + private void testSerializeAndDeserializeCompactV2(BigInteger chainWork) { StoredBlock blockToStore = new StoredBlock(block, chainWork, 0); ByteBuffer buf = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_V2); blockToStore.serializeCompactV2(buf); // assert serialized size and that the buffer is full assertEquals(StoredBlock.COMPACT_SERIALIZED_SIZE_V2, buf.position()); + + buf.rewind(); + StoredBlock deserializedBlock = StoredBlock.deserializeCompactV2(mainnet, buf); + assertEquals(deserializedBlock, blockToStore); } @Test From f8e8e8258ebbb2046f6609ffe147ccca884ef751 Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Tue, 12 Nov 2024 18:49:52 -0400 Subject: [PATCH 07/23] - Renamed deserializeCompact to deserializeCompactLegacy - Deprecated deserializeCompactLegacy --- .../main/java/org/bitcoinj/core/CheckpointManager.java | 4 ++-- core/src/main/java/org/bitcoinj/core/StoredBlock.java | 5 ++++- .../main/java/org/bitcoinj/store/LevelDBBlockStore.java | 2 +- .../org/bitcoinj/store/LevelDBFullPrunedBlockStore.java | 2 +- core/src/main/java/org/bitcoinj/store/SPVBlockStore.java | 2 +- core/src/test/java/org/bitcoinj/core/StoredBlockTest.java | 8 ++++---- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/CheckpointManager.java b/core/src/main/java/org/bitcoinj/core/CheckpointManager.java index 8b8db387dfd..7dc148b55f6 100644 --- a/core/src/main/java/org/bitcoinj/core/CheckpointManager.java +++ b/core/src/main/java/org/bitcoinj/core/CheckpointManager.java @@ -137,7 +137,7 @@ private Sha256Hash readBinary(InputStream inputStream) throws IOException { for (int i = 0; i < numCheckpoints; i++) { if (dis.read(buffer.array(), 0, size) < size) throw new IOException("Incomplete read whilst loading checkpoints."); - StoredBlock block = StoredBlock.deserializeCompact(params, buffer); + StoredBlock block = StoredBlock.deserializeCompactLegacy(params, buffer); buffer.position(0); checkpoints.put(block.getHeader().getTimeSeconds(), block); } @@ -176,7 +176,7 @@ private Sha256Hash readTextual(InputStream inputStream) throws IOException { buffer.position(0); buffer.put(bytes); buffer.position(0); - StoredBlock block = StoredBlock.deserializeCompact(params, buffer); + StoredBlock block = StoredBlock.deserializeCompactLegacy(params, buffer); checkpoints.put(block.getHeader().getTimeSeconds(), block); } HashCode hash = hasher.hash(); diff --git a/core/src/main/java/org/bitcoinj/core/StoredBlock.java b/core/src/main/java/org/bitcoinj/core/StoredBlock.java index 0be255dcb1b..b9cc40663cd 100644 --- a/core/src/main/java/org/bitcoinj/core/StoredBlock.java +++ b/core/src/main/java/org/bitcoinj/core/StoredBlock.java @@ -156,6 +156,8 @@ public void serializeCompactV2(ByteBuffer buffer) { } /** + * @deprecated Use {@link #deserializeCompactV2(NetworkParameters, ByteBuffer)} instead. + * * Deserializes the stored block from a custom packed format. Used internally. * As of June 22, 2024, it takes 12 unsigned bytes to store the chain work value, * so developers should use the V2 format. @@ -163,7 +165,8 @@ public void serializeCompactV2(ByteBuffer buffer) { * @param buffer data to deserialize * @return deserialized stored block */ - public static StoredBlock deserializeCompact(NetworkParameters params, ByteBuffer buffer) throws ProtocolException { + @Deprecated + public static StoredBlock deserializeCompactLegacy(NetworkParameters params, ByteBuffer buffer) throws ProtocolException { byte[] chainWorkBytes = new byte[StoredBlock.CHAIN_WORK_BYTES_V1]; buffer.get(chainWorkBytes); BigInteger chainWork = new BigInteger(1, chainWorkBytes); diff --git a/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java b/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java index febcd157ffe..186a0a39eea 100644 --- a/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java @@ -88,7 +88,7 @@ public synchronized StoredBlock get(Sha256Hash hash) throws BlockStoreException byte[] bits = db.get(hash.getBytes()); if (bits == null) return null; - return StoredBlock.deserializeCompact(context.getParams(), ByteBuffer.wrap(bits)); + return StoredBlock.deserializeCompactLegacy(context.getParams(), ByteBuffer.wrap(bits)); } @Override diff --git a/core/src/main/java/org/bitcoinj/store/LevelDBFullPrunedBlockStore.java b/core/src/main/java/org/bitcoinj/store/LevelDBFullPrunedBlockStore.java index 6f694333713..e9d5be78a63 100644 --- a/core/src/main/java/org/bitcoinj/store/LevelDBFullPrunedBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/LevelDBFullPrunedBlockStore.java @@ -640,7 +640,7 @@ public StoredBlock get(Sha256Hash hash, boolean wasUndoableOnly) throws BlockSto } // TODO Should I chop the last byte off? Seems to work with it left // there... - StoredBlock stored = StoredBlock.deserializeCompact(params, ByteBuffer.wrap(result)); + StoredBlock stored = StoredBlock.deserializeCompactLegacy(params, ByteBuffer.wrap(result)); stored.getHeader().verifyHeader(); if (instrument) diff --git a/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java b/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java index e49f1db1735..74747c92624 100644 --- a/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java @@ -233,7 +233,7 @@ public StoredBlock get(Sha256Hash hash) throws BlockStoreException { buffer.get(scratch); if (Arrays.equals(scratch, targetHashBytes)) { // Found the target. - StoredBlock storedBlock = StoredBlock.deserializeCompact(params, buffer); + StoredBlock storedBlock = StoredBlock.deserializeCompactLegacy(params, buffer); blockCache.put(hash, storedBlock); return storedBlock; } diff --git a/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java b/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java index b4bfe97ac04..092d811bf6b 100644 --- a/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java +++ b/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java @@ -76,7 +76,7 @@ public void serializeAndDeserializeCompactLegacy_forZeroChainWork_works() { // deserialize block blockBuffer.rewind(); - StoredBlock blockDeserialized = StoredBlock.deserializeCompact(mainnet, blockBuffer); + StoredBlock blockDeserialized = StoredBlock.deserializeCompactLegacy(mainnet, blockBuffer); assertEquals(blockDeserialized, blockToStore); } @@ -90,7 +90,7 @@ public void serializeAndDeserializeCompactLegacy_forSmallChainWork_works() { // deserialize block blockBuffer.rewind(); - StoredBlock blockDeserialized = StoredBlock.deserializeCompact(mainnet, blockBuffer); + StoredBlock blockDeserialized = StoredBlock.deserializeCompactLegacy(mainnet, blockBuffer); assertEquals(blockDeserialized, blockToStore); } @@ -104,7 +104,7 @@ public void serializeAndDeserializeCompactLegacy_for8BytesChainWork_works() { // deserialize block blockBuffer.rewind(); - StoredBlock blockDeserialized = StoredBlock.deserializeCompact(mainnet, blockBuffer); + StoredBlock blockDeserialized = StoredBlock.deserializeCompactLegacy(mainnet, blockBuffer); assertEquals(blockDeserialized, blockToStore); } @@ -118,7 +118,7 @@ public void serializeAndDeserializeCompactLegacy_forMax12BytesChainWork_works() // deserialize block blockBuffer.rewind(); - StoredBlock blockDeserialized = StoredBlock.deserializeCompact(mainnet, blockBuffer); + StoredBlock blockDeserialized = StoredBlock.deserializeCompactLegacy(mainnet, blockBuffer); assertEquals(blockDeserialized, blockToStore); } From b780f11e27339cc3f596b5af13cb123083d02423 Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Fri, 8 Nov 2024 15:17:26 -0400 Subject: [PATCH 08/23] =?UTF-8?q?-=20Update=20CheckpointManager=20descript?= =?UTF-8?q?ion=20comment=20-=20Deprecate=20CheckpointManager.readBinary?= =?UTF-8?q?=C2=A0=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/bitcoinj/core/CheckpointManager.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/CheckpointManager.java b/core/src/main/java/org/bitcoinj/core/CheckpointManager.java index 7dc148b55f6..c4d66b6dd0a 100644 --- a/core/src/main/java/org/bitcoinj/core/CheckpointManager.java +++ b/core/src/main/java/org/bitcoinj/core/CheckpointManager.java @@ -60,14 +60,14 @@ * different concept of checkpoints that are used to hard-code the validity of blocks that violate BIP30 (duplicate * coinbase transactions). Those "checkpoints" can be found in NetworkParameters.

* - *

The file format consists of the string "CHECKPOINTS 1", followed by a uint32 containing the number of signatures - * to read. The value may not be larger than 256 (so it could have been a byte but isn't for historical reasons). + *

Checkpoints are read from a text file, one value per line. + * It consists of the magic string "TXT CHECKPOINTS 1", followed by the number of signatures + * to read. The value may not be larger than 256. * If the number of signatures is larger than zero, each 65 byte ECDSA secp256k1 signature then follows. The signatures * sign the hash of all bytes that follow the last signature.

* - *

After the signatures come an int32 containing the number of checkpoints in the file. Then each checkpoint follows - * one after the other. A checkpoint is 12 bytes for the total work done field, 4 bytes for the height, 80 bytes - * for the block header and then 1 zero byte at the end (i.e. number of transactions in the block: always zero).

+ *

After the signatures come the number of checkpoints in the file. Then each checkpoint follows one per line in + * compact format (as written by {@link StoredBlock#serializeCompactV2(ByteBuffer)}) as a base64-encoded blob.

*/ public class CheckpointManager { private static final Logger log = LoggerFactory.getLogger(CheckpointManager.class); @@ -112,6 +112,10 @@ public static InputStream openStream(NetworkParameters params) { return CheckpointManager.class.getResourceAsStream("/" + params.getId() + ".checkpoints.txt"); } + /** @deprecated Use {@link #readTextual(InputStream)} + * The binary format does not support mixed stored block sizes. + * After implementing support to 32-byte chain work to StoredBlock class, + this method cannot read blocks which chain work surpassed 12 byte. */ private Sha256Hash readBinary(InputStream inputStream) throws IOException { DataInputStream dis = null; try { From 4c740807642359a54b089de63327f8f0a1ab9424 Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Tue, 12 Nov 2024 13:33:10 -0400 Subject: [PATCH 09/23] =?UTF-8?q?-=20Update=20readBinary=20method=20to=20t?= =?UTF-8?q?hrow=20an=20exception=20when=20processed=20malformed=C2=A0=20ch?= =?UTF-8?q?eckpoints=20file=20-=20Add=20read=20binary=20checkpoints=20unit?= =?UTF-8?q?=20tests=20-=20Add=20testnet=20and=20mainnet=20binary=20sample?= =?UTF-8?q?=20file=20for=20unit=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/bitcoinj/core/CheckpointManager.java | 11 ++ .../bitcoinj/core/CheckpointManagerTest.java | 158 ++++++++++++++++++ .../org/bitcoinj/core/StoredBlockTest.java | 2 +- .../org.bitcoin.production.checkpoints | Bin 0 -> 23829 bytes .../org.bitcoin.test.checkpoints | Bin 0 -> 72117 bytes 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/org.bitcoin.production.checkpoints create mode 100644 core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/org.bitcoin.test.checkpoints diff --git a/core/src/main/java/org/bitcoinj/core/CheckpointManager.java b/core/src/main/java/org/bitcoinj/core/CheckpointManager.java index c4d66b6dd0a..eaf862ede21 100644 --- a/core/src/main/java/org/bitcoinj/core/CheckpointManager.java +++ b/core/src/main/java/org/bitcoinj/core/CheckpointManager.java @@ -145,6 +145,17 @@ private Sha256Hash readBinary(InputStream inputStream) throws IOException { buffer.position(0); checkpoints.put(block.getHeader().getTimeSeconds(), block); } + + int actualCheckpointsSize = dis.available(); + int expectedCheckpointsSize = numCheckpoints * size; + // Check if there are any bytes left in the stream. If it does, it means that checkpoints are malformed + if (dis.available() > 0) { + String message = String.format( + "Checkpoints size did not match size for version 1 format. Expected checkpoints %d with size of %d bytes, but actual size was %d.", + numCheckpoints, expectedCheckpointsSize, actualCheckpointsSize); + throw new IOException(message); + } + Sha256Hash dataHash = Sha256Hash.wrap(digest.digest()); log.info("Read {} checkpoints up to time {}, hash is {}", checkpoints.size(), Utils.dateTimeFormat(checkpoints.lastEntry().getKey() * 1000), dataHash); diff --git a/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java b/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java index 2690bf29261..ffeee98e890 100644 --- a/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java +++ b/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java @@ -16,19 +16,56 @@ package org.bitcoinj.core; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.InputStream; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.DigestOutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import org.easymock.EasyMockRunner; import org.easymock.Mock; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import java.io.IOException; +import static org.bitcoinj.core.CheckpointManager.BASE64; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.replay; @RunWith(EasyMockRunner.class) public class CheckpointManagerTest { + private static final NetworkParameters MAINNET = NetworkParameters.fromID( + NetworkParameters.ID_MAINNET); + private static final NetworkParameters TESTNET = NetworkParameters.fromID( + NetworkParameters.ID_TESTNET); + private static final BigInteger MAX_WORK_V1 = new BigInteger("ffffffffffffffffffffffff", 16); + + private static final String BINARY_FORMAT_PREFIX = "CHECKPOINTS 1"; + + private static final List CHECKPOINTS_12_BYTES_CHAINWORK_ENCODED = Arrays.asList( + "AAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO", + "AAAAAAAAD8QPxA/EAAAPwAEAAADHtJ8Nq3z30grJ9lTH6bLhKSHX+MxmkZn8z5wuAAAAAK0gXcQFtYSj/IB2KZ38+itS1Da0Dn/3XosOFJntz7A8OsC/T8D/Pxwf0no+", + "AAAAAAAALUAtQC1AAAAXoAEAAABwvpBfmfp76xvcOzhdR+OPnJ2aLD5znGpD8LkJAAAAALkv0fxOJYZ1dMLCyDV+3AB0y+BW8lP5/8xBMMqLbX7u+gPDT/D/DxwDvhrh" + ); + + private static final List CHECKPOINTS_32_BYTES_CHAINWORK_ENCODED = Arrays.asList( + // 13 bytes TOO_LARGE_WORK_V1 = ffffffffffffffffffffffffff + "AAAAAAAAAAAAAAAAAAAAAAAAAP////////////////8AAB+AAQAAANW+iGqrr/fsekjWfL7yhyKCGSieKwRG8nmcnAoAAAAAFEW4aog6zdt5sMVmp3UMo/H/JkXiG/u3vmsfyYvo5ThKBcNPwP8/HBEzbVs", + "AAAAAAAAAAAAAAAAAAAAAAAAAP////////////////8AACdgAQAAAAnfZFAmRFbc2clq5XzNV2/UbKPLCAB7JOECcDoAAAAAeCpL87HF9/JFao8VX1rqRU/pMsv8F08X8ieq464NqECaBsNP//8AHRvpMAo", + "AAAAAAAAAAAAAAAAAAAAAAAAAP////////////////8AAC9AAQAAAMipH0cUa3D2Ea/T7sCMt0G4Tuqq5/b/KugBHgYAAAAAIROhXYS8rkGyrLjTJvp2iWRfTDOcu/Rkkf9Az5xpTLjrB8NPwP8/HGbjgbo", + // 32 bytes MAX_WORK_V2 = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + "//////////////////////////////////////////8AAB+AAQAAANW+iGqrr/fsekjWfL7yhyKCGSieKwRG8nmcnAoAAAAAFEW4aog6zdt5sMVmp3UMo/H/JkXiG/u3vmsfyYvo5ThKBcNPwP8/HBEzbVs", + "//////////////////////////////////////////8AACdgAQAAAAnfZFAmRFbc2clq5XzNV2/UbKPLCAB7JOECcDoAAAAAeCpL87HF9/JFao8VX1rqRU/pMsv8F08X8ieq464NqECaBsNP//8AHRvpMAo", + "//////////////////////////////////////////8AAC9AAQAAAMipH0cUa3D2Ea/T7sCMt0G4Tuqq5/b/KugBHgYAAAAAIROhXYS8rkGyrLjTJvp2iWRfTDOcu/Rkkf9Az5xpTLjrB8NPwP8/HGbjgbo" + ); + @Mock NetworkParameters params; @@ -63,4 +100,125 @@ public void canReadTextualStream() throws IOException { replay(params); new CheckpointManager(params, null); } + + @Test + public void readBinaryCheckpoint_whenTestnet_ok() throws IOException { + readBinaryCheckpoint(TESTNET, + "org/bitcoinj/core/checkpointmanagertest/org.bitcoin.test.checkpoints"); + } + + private void readBinaryCheckpoint(NetworkParameters networkParameters, + String checkpointPath) throws IOException { + InputStream checkpointStream = getClass().getResourceAsStream(checkpointPath); + new CheckpointManager(networkParameters, checkpointStream); + } + + @Test + public void readBinaryCheckpoint_whenMainnet_ok() throws IOException { + readBinaryCheckpoint(MAINNET, + "org/bitcoinj/core/checkpointmanagertest/org.bitcoin.production.checkpoints"); + } + + @Test + public void readBinaryCheckpoints_whenCheckpointChainWorkIs12Bytes() throws IOException { + List checkpoints = getCheckpoints(CHECKPOINTS_12_BYTES_CHAINWORK_ENCODED, + StoredBlock.COMPACT_SERIALIZED_SIZE); + try (InputStream binaryCheckpoint = generateBinaryCheckpoints(checkpoints)) { + CheckpointManager checkpointManager = new CheckpointManager(MAINNET, binaryCheckpoint); + + List actualCheckpoints = new ArrayList<>(checkpointManager.checkpoints.values()); + Assert.assertEquals(checkpoints, actualCheckpoints); + } + } + + private List getCheckpoints(List checkpoints, int blockFormatSize) { + ByteBuffer buffer = ByteBuffer.allocate(blockFormatSize); + List decodedCheckpoints = new ArrayList(); + for (String checkpoint : checkpoints) { + byte[] bytes = BASE64.decode(checkpoint); + buffer.clear(); + buffer.put(bytes); + buffer.flip(); + StoredBlock block; + if (blockFormatSize == StoredBlock.COMPACT_SERIALIZED_SIZE) { + block = StoredBlock.deserializeCompact(MAINNET, buffer); + } else { + block = StoredBlock.deserializeCompactV2(MAINNET, buffer); + } + decodedCheckpoints.add(block); + } + return decodedCheckpoints; + } + + private void serializeBlock(ByteBuffer buffer, StoredBlock block, boolean isV1) + throws IOException { + buffer.rewind(); + if (isV1) { + block.serializeCompact(buffer); + } else { + block.serializeCompactV2(buffer); + } + } + + private InputStream generateBinaryCheckpoints(List checkpoints) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + DigestOutputStream digestStream = new DigestOutputStream(outputStream, + Sha256Hash.newDigest()); + DataOutputStream dataStream = new DataOutputStream(digestStream)) { + + digestStream.on(false); + dataStream.writeBytes(BINARY_FORMAT_PREFIX); + dataStream.writeInt(0); + digestStream.on(true); + dataStream.writeInt(checkpoints.size()); + + ByteBuffer bufferV1 = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); + ByteBuffer bufferV2 = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_V2); + + for (StoredBlock block : checkpoints) { + boolean isV1 = block.getChainWork().compareTo(MAX_WORK_V1) <= 0; + ByteBuffer buffer = isV1 ? bufferV1 : bufferV2; + serializeBlock(buffer, block, isV1); + int limit = isV1 ? StoredBlock.COMPACT_SERIALIZED_SIZE + : StoredBlock.COMPACT_SERIALIZED_SIZE_V2; + dataStream.write(buffer.array(), 0, limit); + } + return new ByteArrayInputStream(outputStream.toByteArray()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test(expected = IOException.class) + public void readBinaryCheckpoints_whenV2Format_shouldFail() throws IOException { + List checkpointsV2Format = getCheckpoints(CHECKPOINTS_32_BYTES_CHAINWORK_ENCODED, + StoredBlock.COMPACT_SERIALIZED_SIZE_V2); + try (InputStream binaryCheckpoint = generateBinaryCheckpoints(checkpointsV2Format)) { + CheckpointManager checkpointManager = new CheckpointManager(MAINNET, binaryCheckpoint); + + List actualCheckpoints = new ArrayList<>(checkpointManager.checkpoints.values()); + + Assert.assertNotEquals(checkpointsV2Format, actualCheckpoints); + } + } + + @Test(expected = IOException.class) + public void readBinaryCheckpoints_whenMixFormats_shouldFail() + throws IOException { + List checkpointsV1Format = getCheckpoints(CHECKPOINTS_12_BYTES_CHAINWORK_ENCODED, + StoredBlock.COMPACT_SERIALIZED_SIZE); + List checkpointsV2Format = getCheckpoints(CHECKPOINTS_32_BYTES_CHAINWORK_ENCODED, + StoredBlock.COMPACT_SERIALIZED_SIZE_V2); + + List checkpoints = new ArrayList<>(); + checkpoints.addAll(checkpointsV1Format); + checkpoints.addAll(checkpointsV2Format); + try (InputStream binaryCheckpoint = generateBinaryCheckpoints(checkpoints)) { + CheckpointManager checkpointManager = new CheckpointManager(MAINNET, binaryCheckpoint); + + List actualCheckpoints = new ArrayList<>(checkpointManager.checkpoints.values()); + + Assert.assertNotEquals(checkpointsV2Format, actualCheckpoints); + } + } } diff --git a/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java b/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java index 092d811bf6b..cfd3ea02655 100644 --- a/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java +++ b/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java @@ -149,7 +149,7 @@ public void serializeCompactV2_forNegativeChainWork_throwsException() { @Test(expected = IllegalArgumentException.class) public void serializeCompactV2_forMoreThan32bytesChainWork_throwsException() { - StoredBlock blockToStore = new StoredBlock(block, TOO_LARGE_WORK_V2, blockHeight); + StoredBlock blockToStore = new StoredBlock(block, TOO_LARGE_WORK_V2, blockHeight); // serialize block should throw illegal argument exception blockToStore.serializeCompactV2(blockBuffer); diff --git a/core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/org.bitcoin.production.checkpoints b/core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/org.bitcoin.production.checkpoints new file mode 100644 index 0000000000000000000000000000000000000000..c4a1b1ecfb3a2c7ab413f578c0f6c54ccba76cb3 GIT binary patch literal 23829 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

ARzAZB6+sh3~{2^8O9M^i%T1KkH=lL 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 Qll7UNL}DV=JGiL-2jwe{XaE2J literal 0 HcmV?d00001 From 41a27643369a4d790487c450b2799bc19cf6f8bf Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Tue, 12 Nov 2024 15:14:57 -0400 Subject: [PATCH 10/23] Fix issues after rebased --- core/src/main/java/org/bitcoinj/core/CheckpointManager.java | 1 + .../test/java/org/bitcoinj/core/CheckpointManagerTest.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/CheckpointManager.java b/core/src/main/java/org/bitcoinj/core/CheckpointManager.java index eaf862ede21..6cde43540eb 100644 --- a/core/src/main/java/org/bitcoinj/core/CheckpointManager.java +++ b/core/src/main/java/org/bitcoinj/core/CheckpointManager.java @@ -116,6 +116,7 @@ public static InputStream openStream(NetworkParameters params) { * The binary format does not support mixed stored block sizes. * After implementing support to 32-byte chain work to StoredBlock class, this method cannot read blocks which chain work surpassed 12 byte. */ + @Deprecated private Sha256Hash readBinary(InputStream inputStream) throws IOException { DataInputStream dis = null; try { diff --git a/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java b/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java index ffeee98e890..7803f2cfbc9 100644 --- a/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java +++ b/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java @@ -141,7 +141,7 @@ private List getCheckpoints(List checkpoints, int blockForm buffer.flip(); StoredBlock block; if (blockFormatSize == StoredBlock.COMPACT_SERIALIZED_SIZE) { - block = StoredBlock.deserializeCompact(MAINNET, buffer); + block = StoredBlock.deserializeCompactLegacy(MAINNET, buffer); } else { block = StoredBlock.deserializeCompactV2(MAINNET, buffer); } @@ -154,7 +154,7 @@ private void serializeBlock(ByteBuffer buffer, StoredBlock block, boolean isV1) throws IOException { buffer.rewind(); if (isV1) { - block.serializeCompact(buffer); + block.serializeCompactLegacy(buffer); } else { block.serializeCompactV2(buffer); } From 3e6d064c59c77d6e7bd4af3c8a7993e51baa3548 Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Wed, 13 Nov 2024 01:00:06 -0400 Subject: [PATCH 11/23] - Add support to 32 bytes chain work to textual checkpoints format - Add tests for textual format reading --- .../org/bitcoinj/core/CheckpointManager.java | 15 ++-- .../bitcoinj/core/CheckpointManagerTest.java | 77 +++++++++---------- .../mixFormats.checkpoints.txt | 11 +++ .../validTextualFormat.checkpoints.txt | 9 ++- 4 files changed, 63 insertions(+), 49 deletions(-) create mode 100644 core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/mixFormats.checkpoints.txt diff --git a/core/src/main/java/org/bitcoinj/core/CheckpointManager.java b/core/src/main/java/org/bitcoinj/core/CheckpointManager.java index 6cde43540eb..92fd810751b 100644 --- a/core/src/main/java/org/bitcoinj/core/CheckpointManager.java +++ b/core/src/main/java/org/bitcoinj/core/CheckpointManager.java @@ -184,15 +184,18 @@ private Sha256Hash readTextual(InputStream inputStream) throws IOException { checkState(numCheckpoints > 0); // Hash numCheckpoints in a way compatible to the binary format. hasher.putBytes(ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(numCheckpoints).array()); - final int size = StoredBlock.COMPACT_SERIALIZED_SIZE; - ByteBuffer buffer = ByteBuffer.allocate(size); for (int i = 0; i < numCheckpoints; i++) { byte[] bytes = BASE64.decode(reader.readLine()); hasher.putBytes(bytes); - buffer.position(0); - buffer.put(bytes); - buffer.position(0); - StoredBlock block = StoredBlock.deserializeCompactLegacy(params, buffer); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + StoredBlock block; + if (bytes.length == StoredBlock.COMPACT_SERIALIZED_SIZE){ + block = StoredBlock.deserializeCompactLegacy(params, buffer); + } else if (bytes.length == StoredBlock.COMPACT_SERIALIZED_SIZE_V2) { + block = StoredBlock.deserializeCompactV2(params, buffer); + } else { + throw new IllegalStateException("unexpected length of checkpoint: " + bytes.length); + } checkpoints.put(block.getHeader().getTimeSeconds(), block); } HashCode hash = hasher.hash(); diff --git a/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java b/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java index 7803f2cfbc9..cde4a2480d6 100644 --- a/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java +++ b/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java @@ -26,19 +26,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.easymock.EasyMockRunner; -import org.easymock.Mock; import org.junit.Assert; import org.junit.Test; -import org.junit.runner.RunWith; import java.io.IOException; import static org.bitcoinj.core.CheckpointManager.BASE64; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.replay; -@RunWith(EasyMockRunner.class) public class CheckpointManagerTest { private static final NetworkParameters MAINNET = NetworkParameters.fromID( @@ -66,41 +60,6 @@ public class CheckpointManagerTest { "//////////////////////////////////////////8AAC9AAQAAAMipH0cUa3D2Ea/T7sCMt0G4Tuqq5/b/KugBHgYAAAAAIROhXYS8rkGyrLjTJvp2iWRfTDOcu/Rkkf9Az5xpTLjrB8NPwP8/HGbjgbo" ); - @Mock - NetworkParameters params; - - @Test(expected = NullPointerException.class) - public void shouldThrowNullPointerExceptionWhenCheckpointsNotFound() throws IOException { - expect(params.getId()).andReturn("org/bitcoinj/core/checkpointmanagertest/notFound"); - replay(params); - new CheckpointManager(params, null); - } - - @Test(expected = IOException.class) - public void shouldThrowNullPointerExceptionWhenCheckpointsInUnknownFormat() throws IOException { - expect(params.getId()).andReturn("org/bitcoinj/core/checkpointmanagertest/unsupportedFormat"); - replay(params); - new CheckpointManager(params, null); - } - - @Test(expected = IllegalStateException.class) - public void shouldThrowIllegalStateExceptionWithNoCheckpoints() throws IOException { - expect(params.getId()).andReturn("org/bitcoinj/core/checkpointmanagertest/noCheckpoints"); - replay(params); - new CheckpointManager(params, null); - } - - @Test - public void canReadTextualStream() throws IOException { - expect(params.getId()).andReturn("org/bitcoinj/core/checkpointmanagertest/validTextualFormat"); - expect(params.getSerializer(false)).andReturn( - new BitcoinSerializer(params, false)); - expect(params.getProtocolVersionNum(NetworkParameters.ProtocolVersion.CURRENT)) - .andReturn(NetworkParameters.ProtocolVersion.CURRENT.getBitcoinProtocolVersion()); - replay(params); - new CheckpointManager(params, null); - } - @Test public void readBinaryCheckpoint_whenTestnet_ok() throws IOException { readBinaryCheckpoint(TESTNET, @@ -221,4 +180,40 @@ public void readBinaryCheckpoints_whenMixFormats_shouldFail() Assert.assertNotEquals(checkpointsV2Format, actualCheckpoints); } } + + @Test(expected = NullPointerException.class) + public void shouldThrowNullPointerExceptionWhenCheckpointsNotFound() throws IOException { + InputStream checkpointStream = getClass().getResourceAsStream("/org/bitcoinj/core/checkpointmanagertest/notFound.checkpoints.txt"); + new CheckpointManager(null, checkpointStream); + } + + @Test(expected = IOException.class) + public void shouldThrowNullPointerExceptionWhenCheckpointsInUnknownFormat() throws IOException { + InputStream checkpointStream = getClass().getResourceAsStream("/org/bitcoinj/core/checkpointmanagertest/unsupportedFormat.checkpoints.txt"); + new CheckpointManager(MAINNET, checkpointStream); + } + + @Test(expected = IllegalStateException.class) + public void shouldThrowIllegalStateExceptionWithNoCheckpoints() throws IOException { + InputStream checkpointStream = getClass().getResourceAsStream("/org/bitcoinj/core/checkpointmanagertest/noCheckpoints.checkpoints.txt"); + new CheckpointManager(MAINNET, checkpointStream); + } + + @Test + public void canReadTextualFormat() throws IOException { + InputStream checkpointStream = getClass().getResourceAsStream("/org/bitcoinj/core/checkpointmanagertest/validTextualFormat.checkpoints.txt"); + CheckpointManager checkpointManager = new CheckpointManager(MAINNET, checkpointStream); + + List actualCheckpoints = new ArrayList<>(checkpointManager.checkpoints.values()); + Assert.assertEquals(6, actualCheckpoints.size()); + } + + @Test + public void canReadTextualMixFormats() throws IOException { + InputStream checkpointStream = getClass().getResourceAsStream("/org/bitcoinj/core/checkpointmanagertest/mixFormats.checkpoints.txt"); + CheckpointManager checkpointManager = new CheckpointManager(MAINNET, checkpointStream); + + List actualCheckpoints = new ArrayList<>(checkpointManager.checkpoints.values()); + Assert.assertEquals(6, actualCheckpoints.size()); + } } diff --git a/core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/mixFormats.checkpoints.txt b/core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/mixFormats.checkpoints.txt new file mode 100644 index 00000000000..91fe9e58504 --- /dev/null +++ b/core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/mixFormats.checkpoints.txt @@ -0,0 +1,11 @@ +TXT CHECKPOINTS 1 +0 +6 +AAAAAAAAAAAAAAAAAAAAAAAAAP////////////////8AAC9AAQAAAMipH0cUa3D2Ea/T7sCMt0G4Tuqq5/b/KugBHgYAAAAAIROhXYS8rkGyrLjTJvp2iWRfTDOcu/Rkkf9Az5xpTLjrB8NPwP8/HGbjgbo +AAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO +AAAAAAAAD8QPxA/EAAAPwAEAAADHtJ8Nq3z30grJ9lTH6bLhKSHX+MxmkZn8z5wuAAAAAK0gXcQFtYSj/IB2KZ38+itS1Da0Dn/3XosOFJntz7A8OsC/T8D/Pxwf0no+ +AAAAAAAALUAtQC1AAAAXoAEAAABwvpBfmfp76xvcOzhdR+OPnJ2aLD5znGpD8LkJAAAAALkv0fxOJYZ1dMLCyDV+3AB0y+BW8lP5/8xBMMqLbX7u+gPDT/D/DxwDvhrh +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACrJaslqyUAAB+AAQAAANW+iGqrr/fsekjWfL7yhyKCGSieKwRG8nmcnAoAAAAAFEW4aog6zdt5sMVmp3UMo/H/JkXiG/u3vmsfyYvo5ThKBcNPwP8/HBEzbVs +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADKn8qfyp8AACdgAQAAAAnfZFAmRFbc2clq5XzNV2/UbKPLCAB7JOECcDoAAAAAeCpL87HF9/JFao8VX1rqRU/pMsv8F08X8ieq464NqECaBsNP//8AHRvpMAo +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADSgtKC0oIAAC9AAQAAAMipH0cUa3D2Ea/T7sCMt0G4Tuqq5/b/KugBHgYAAAAAIROhXYS8rkGyrLjTJvp2iWRfTDOcu/Rkkf9Az5xpTLjrB8NPwP8/HGbjgbo +//////////////////////////////////////////8AAB+AAQAAANW+iGqrr/fsekjWfL7yhyKCGSieKwRG8nmcnAoAAAAAFEW4aog6zdt5sMVmp3UMo/H/JkXiG/u3vmsfyYvo5ThKBcNPwP8/HBEzbVs \ No newline at end of file diff --git a/core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/validTextualFormat.checkpoints.txt b/core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/validTextualFormat.checkpoints.txt index be997738eef..6014cf3c2ae 100644 --- a/core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/validTextualFormat.checkpoints.txt +++ b/core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/validTextualFormat.checkpoints.txt @@ -1,4 +1,9 @@ TXT CHECKPOINTS 1 0 -1 -AAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO \ No newline at end of file +6 +AAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO +AAAAAAAAD8QPxA/EAAAPwAEAAADHtJ8Nq3z30grJ9lTH6bLhKSHX+MxmkZn8z5wuAAAAAK0gXcQFtYSj/IB2KZ38+itS1Da0Dn/3XosOFJntz7A8OsC/T8D/Pxwf0no+ +AAAAAAAALUAtQC1AAAAXoAEAAABwvpBfmfp76xvcOzhdR+OPnJ2aLD5znGpD8LkJAAAAALkv0fxOJYZ1dMLCyDV+3AB0y+BW8lP5/8xBMMqLbX7u+gPDT/D/DxwDvhrh +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACrJaslqyUAAB+AAQAAANW+iGqrr/fsekjWfL7yhyKCGSieKwRG8nmcnAoAAAAAFEW4aog6zdt5sMVmp3UMo/H/JkXiG/u3vmsfyYvo5ThKBcNPwP8/HBEzbVs +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADKn8qfyp8AACdgAQAAAAnfZFAmRFbc2clq5XzNV2/UbKPLCAB7JOECcDoAAAAAeCpL87HF9/JFao8VX1rqRU/pMsv8F08X8ieq464NqECaBsNP//8AHRvpMAo +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADSgtKC0oIAAC9AAQAAAMipH0cUa3D2Ea/T7sCMt0G4Tuqq5/b/KugBHgYAAAAAIROhXYS8rkGyrLjTJvp2iWRfTDOcu/Rkkf9Az5xpTLjrB8NPwP8/HGbjgbo \ No newline at end of file From f2cbe5b57274fc52ace5be2f91c4dee41855bbd1 Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Thu, 14 Nov 2024 11:02:04 -0400 Subject: [PATCH 12/23] Fix format --- core/src/main/java/org/bitcoinj/core/CheckpointManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/bitcoinj/core/CheckpointManager.java b/core/src/main/java/org/bitcoinj/core/CheckpointManager.java index 92fd810751b..81d17da56a7 100644 --- a/core/src/main/java/org/bitcoinj/core/CheckpointManager.java +++ b/core/src/main/java/org/bitcoinj/core/CheckpointManager.java @@ -189,7 +189,7 @@ private Sha256Hash readTextual(InputStream inputStream) throws IOException { hasher.putBytes(bytes); ByteBuffer buffer = ByteBuffer.wrap(bytes); StoredBlock block; - if (bytes.length == StoredBlock.COMPACT_SERIALIZED_SIZE){ + if (bytes.length == StoredBlock.COMPACT_SERIALIZED_SIZE) { block = StoredBlock.deserializeCompactLegacy(params, buffer); } else if (bytes.length == StoredBlock.COMPACT_SERIALIZED_SIZE_V2) { block = StoredBlock.deserializeCompactV2(params, buffer); From 9f7884a6c29399a2c1058a09bbc943c40a8df09a Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Wed, 13 Nov 2024 23:34:32 -0400 Subject: [PATCH 13/23] Remove binary format support in BuildCheckpoints --- .../org/bitcoinj/tools/BuildCheckpoints.java | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java b/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java index 4ea5a0bf471..dc586ff5849 100644 --- a/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java +++ b/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java @@ -32,7 +32,6 @@ import joptsimple.OptionSet; import joptsimple.OptionSpec; -import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -43,8 +42,6 @@ import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.security.DigestOutputStream; -import java.security.MessageDigest; import java.util.*; import java.util.concurrent.Future; @@ -156,45 +153,18 @@ public void notifyNewBestBlock(StoredBlock block) throws VerificationException { checkState(checkpoints.size() > 0); - final File plainFile = new File("checkpoints" + suffix); final File textFile = new File("checkpoints" + suffix + ".txt"); // Write checkpoint data out. - writeBinaryCheckpoints(checkpoints, plainFile); writeTextualCheckpoints(checkpoints, textFile); peerGroup.stop(); store.close(); // Sanity check the created files. - sanityCheck(plainFile, checkpoints.size()); sanityCheck(textFile, checkpoints.size()); } - private static void writeBinaryCheckpoints(TreeMap checkpoints, File file) throws Exception { - final FileOutputStream fileOutputStream = new FileOutputStream(file, false); - MessageDigest digest = Sha256Hash.newDigest(); - final DigestOutputStream digestOutputStream = new DigestOutputStream(fileOutputStream, digest); - digestOutputStream.on(false); - final DataOutputStream dataOutputStream = new DataOutputStream(digestOutputStream); - dataOutputStream.writeBytes("CHECKPOINTS 1"); - dataOutputStream.writeInt(0); // Number of signatures to read. Do this later. - digestOutputStream.on(true); - dataOutputStream.writeInt(checkpoints.size()); - ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); - for (StoredBlock block : checkpoints.values()) { - block.serializeCompactLegacy(buffer); - dataOutputStream.write(buffer.array()); - buffer.position(0); - } - dataOutputStream.close(); - Sha256Hash checkpointsHash = Sha256Hash.wrap(digest.digest()); - System.out.println("Hash of checkpoints data is " + checkpointsHash); - digestOutputStream.close(); - fileOutputStream.close(); - System.out.println("Checkpoints written to '" + file.getCanonicalPath() + "'."); - } - private static void writeTextualCheckpoints(TreeMap checkpoints, File file) throws IOException { PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.US_ASCII)); writer.println("TXT CHECKPOINTS 1"); From ead7ec42273141e4300c734d666d5b94f7eba6c5 Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Wed, 13 Nov 2024 23:53:56 -0400 Subject: [PATCH 14/23] Use serializeCompactV2 for textual format in BuildCheckpoints --- .../org/bitcoinj/tools/BuildCheckpoints.java | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java b/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java index dc586ff5849..6559f789a6a 100644 --- a/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java +++ b/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java @@ -17,6 +17,8 @@ package org.bitcoinj.tools; +import java.math.BigInteger; +import java.nio.file.Files; import org.bitcoinj.core.listeners.NewBestBlockListener; import org.bitcoinj.core.*; import org.bitcoinj.net.discovery.DnsDiscovery; @@ -165,16 +167,27 @@ public void notifyNewBestBlock(StoredBlock block) throws VerificationException { sanityCheck(textFile, checkpoints.size()); } - private static void writeTextualCheckpoints(TreeMap checkpoints, File file) throws IOException { - PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.US_ASCII)); + + private static final BigInteger MAX_WORK_V1 = new BigInteger(/* 12 bytes */ "ffffffffffffffffffffffff", 16); + + protected static void writeTextualCheckpoints(TreeMap checkpoints, File file) throws IOException { + PrintWriter writer = new PrintWriter(new OutputStreamWriter( + Files.newOutputStream(file.toPath()), StandardCharsets.US_ASCII)); writer.println("TXT CHECKPOINTS 1"); writer.println("0"); // Number of signatures to read. Do this later. writer.println(checkpoints.size()); - ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); + ByteBuffer bufferV1 = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); + ByteBuffer bufferV2 = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_V2); for (StoredBlock block : checkpoints.values()) { - block.serializeCompactLegacy(buffer); - writer.println(CheckpointManager.BASE64.encode(buffer.array())); - buffer.position(0); + if (block.getChainWork().compareTo(MAX_WORK_V1) <= 0) { + bufferV1.rewind(); + block.serializeCompactLegacy(bufferV1); + writer.println(CheckpointManager.BASE64.encode(bufferV1.array())); + } else { + bufferV2.rewind(); + block.serializeCompactV2(bufferV2); + writer.println(CheckpointManager.BASE64.encode(bufferV2.array())); + } } writer.close(); System.out.println("Checkpoints written to '" + file.getCanonicalPath() + "'."); From 135e445cf2649f6d61976167d5fc5e21aee0dca6 Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Thu, 14 Nov 2024 12:03:17 -0400 Subject: [PATCH 15/23] Add tests for BuildCheckpoints tool --- tools/build.gradle | 2 + .../org/bitcoinj/tools/BuildCheckpoints.java | 1 - .../bitcoinj/tools/BuildCheckpointsTest.java | 161 ++++++++++++++++++ 3 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 tools/src/test/java/org/bitcoinj/tools/BuildCheckpointsTest.java diff --git a/tools/build.gradle b/tools/build.gradle index 0dbb9fca7f7..c6ea8198117 100644 --- a/tools/build.gradle +++ b/tools/build.gradle @@ -8,6 +8,8 @@ dependencies { implementation 'com.google.guava:guava:28.1-android' implementation 'net.sf.jopt-simple:jopt-simple:5.0.4' implementation 'org.slf4j:slf4j-jdk14:1.7.29' + testImplementation 'junit:junit:4.12' + testImplementation 'org.easymock:easymock:3.2' } sourceCompatibility = 1.8 diff --git a/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java b/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java index 6559f789a6a..26deb5e3903 100644 --- a/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java +++ b/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java @@ -36,7 +36,6 @@ import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; diff --git a/tools/src/test/java/org/bitcoinj/tools/BuildCheckpointsTest.java b/tools/src/test/java/org/bitcoinj/tools/BuildCheckpointsTest.java new file mode 100644 index 00000000000..b24c4e3170d --- /dev/null +++ b/tools/src/test/java/org/bitcoinj/tools/BuildCheckpointsTest.java @@ -0,0 +1,161 @@ +package org.bitcoinj.tools; + +import static org.bitcoinj.core.CheckpointManager.BASE64; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.TreeMap; +import org.bitcoinj.core.CheckpointManager; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.StoredBlock; +import org.junit.Before; +import org.junit.Test; + +public class BuildCheckpointsTest { + + private static final NetworkParameters MAINNET = NetworkParameters.fromID(NetworkParameters.ID_MAINNET); + + private static final String TEXT_FORMAT_PREFIX = "TXT CHECKPOINTS 1"; + private static final String DEFAULT_NUM_OF_SIGNATURES_VALUE = "0"; + + private static final List CHECKPOINTS_12_BYTES_CHAINWORK_ENCODED = Arrays.asList( + "AAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO", + "AAAAAAAAD8QPxA/EAAAPwAEAAADHtJ8Nq3z30grJ9lTH6bLhKSHX+MxmkZn8z5wuAAAAAK0gXcQFtYSj/IB2KZ38+itS1Da0Dn/3XosOFJntz7A8OsC/T8D/Pxwf0no+", + "AAAAAAAALUAtQC1AAAAXoAEAAABwvpBfmfp76xvcOzhdR+OPnJ2aLD5znGpD8LkJAAAAALkv0fxOJYZ1dMLCyDV+3AB0y+BW8lP5/8xBMMqLbX7u+gPDT/D/DxwDvhrh" + ); + + private static final List CHECKPOINTS_32_BYTES_CHAINWORK_ENCODED = Arrays.asList( + // 13 bytes TOO_LARGE_WORK_V1 = ffffffffffffffffffffffffff + "AAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAQAAACmbBfpQISclxggpNrsc7ekWSqo2EDMlUEx0S4YAAAAA9IC9UIFTKkJsFb86pTSggXshIXziHayk6oesUJY31nDqvr9P//8AHQTmg44", + "AAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAQAAAMe0nw2rfPfSCsn2VMfpsuEpIdf4zGaRmfzPnC4AAAAArSBdxAW1hKP8gHYpnfz6K1LUNrQOf/deiw4Ume3PsDw6wL9PwP8/HB/Sej4", + "AAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAQAAAHC+kF+Z+nvrG9w7OF1H44+cnZosPnOcakPwuQkAAAAAuS/R/E4lhnV0wsLINX7cAHTL4FbyU/n/zEEwyottfu76A8NP8P8PHAO+GuE", + // 32 bytes MAX_WORK_V2 = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + "//////////////////////////////////////////8AAB+AAQAAANW+iGqrr/fsekjWfL7yhyKCGSieKwRG8nmcnAoAAAAAFEW4aog6zdt5sMVmp3UMo/H/JkXiG/u3vmsfyYvo5ThKBcNPwP8/HBEzbVs", + "//////////////////////////////////////////8AACdgAQAAAAnfZFAmRFbc2clq5XzNV2/UbKPLCAB7JOECcDoAAAAAeCpL87HF9/JFao8VX1rqRU/pMsv8F08X8ieq464NqECaBsNP//8AHRvpMAo", + "//////////////////////////////////////////8AAC9AAQAAAMipH0cUa3D2Ea/T7sCMt0G4Tuqq5/b/KugBHgYAAAAAIROhXYS8rkGyrLjTJvp2iWRfTDOcu/Rkkf9Az5xpTLjrB8NPwP8/HGbjgbo" + ); + + private TreeMap checkpoints; + private File textFile; + + @Before + public void setUp() throws Exception { + checkpoints = new TreeMap<>(); + textFile = new File("checkpoints.txt"); + textFile.delete(); + } + + @Test + public void writeTextualCheckpoints_whenBlocksChainWorkFits12Bytes_shouldBuiltFile() throws IOException { + assertFalse(textFile.exists()); + populateCheckpoints(CHECKPOINTS_12_BYTES_CHAINWORK_ENCODED, StoredBlock.COMPACT_SERIALIZED_SIZE, MAINNET); + BuildCheckpoints.writeTextualCheckpoints(checkpoints, textFile); + assertTrue(textFile.exists()); + + assertCheckpointFileContent(CHECKPOINTS_12_BYTES_CHAINWORK_ENCODED); + + try (FileInputStream checkpointsStream = new FileInputStream(textFile)) { + CheckpointManager checkpointManager = new CheckpointManager(MAINNET, checkpointsStream); + assertEquals(checkpoints.size(), checkpointManager.numCheckpoints()); + } catch (Exception ex) { + fail(); + } + } + + private void assertCheckpointFileContent(List checkpoints12BytesChainworkEncoded) { + List lines = new ArrayList<>(); + try (BufferedReader bufferedReader = new BufferedReader(new FileReader(textFile))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + lines.add(line); + } + } catch (Exception e) { + fail(); + } + + assertEquals(TEXT_FORMAT_PREFIX, lines.get(0)); + assertEquals(DEFAULT_NUM_OF_SIGNATURES_VALUE, lines.get(1)); + assertEquals(checkpoints.size(), Integer.parseInt(lines.get(2))); + for (int index = 3; index < lines.size(); index++) { + assertTrue(checkpoints12BytesChainworkEncoded.contains(lines.get(index))); + } + } + + private void populateCheckpoints(List encodedCheckpoints, int blockFormatSize, + NetworkParameters networkParameters) { + List decodedCheckpoints = getCheckpoints(encodedCheckpoints, blockFormatSize, + networkParameters); + for (int i = 0; i < decodedCheckpoints.size(); i++) { + checkpoints.put(i, decodedCheckpoints.get(i)); + } + } + + private List getCheckpoints(List encodedCheckpoints, int blockFormatSize, + NetworkParameters networkParameters) { + ByteBuffer buffer = ByteBuffer.allocate(blockFormatSize); + List decodedCheckpoints = new ArrayList(); + for (String checkpoint : encodedCheckpoints) { + byte[] bytes = BASE64.decode(checkpoint); + buffer.clear(); + buffer.put(bytes); + buffer.flip(); + StoredBlock block; + if (blockFormatSize == StoredBlock.COMPACT_SERIALIZED_SIZE) { + block = StoredBlock.deserializeCompactLegacy(networkParameters, buffer); + } else { + block = StoredBlock.deserializeCompactV2(networkParameters, buffer); + } + decodedCheckpoints.add(block); + } + return decodedCheckpoints; + } + + @Test + public void writeTextualCheckpoints_whenBlocksChainWorkUse32Bytes_shouldBuiltFile() throws IOException { + assertFalse(textFile.exists()); + populateCheckpoints(CHECKPOINTS_32_BYTES_CHAINWORK_ENCODED, StoredBlock.COMPACT_SERIALIZED_SIZE_V2, MAINNET); + BuildCheckpoints.writeTextualCheckpoints(checkpoints, textFile); + assertTrue(textFile.exists()); + + assertCheckpointFileContent(CHECKPOINTS_32_BYTES_CHAINWORK_ENCODED); + + try (FileInputStream checkpointsStream = new FileInputStream(textFile)) { + CheckpointManager checkpointManager = new CheckpointManager(MAINNET, checkpointsStream); + assertEquals(checkpoints.size(), checkpointManager.numCheckpoints()); + } catch (Exception ex) { + fail(); + } + } + + @Test + public void writeTextualCheckpoints_whenMixBlocksChainWork_shouldBuiltFile() throws IOException { + assertFalse(textFile.exists()); + populateCheckpoints(CHECKPOINTS_12_BYTES_CHAINWORK_ENCODED, StoredBlock.COMPACT_SERIALIZED_SIZE, MAINNET); + populateCheckpoints(CHECKPOINTS_32_BYTES_CHAINWORK_ENCODED, StoredBlock.COMPACT_SERIALIZED_SIZE_V2, MAINNET); + BuildCheckpoints.writeTextualCheckpoints(checkpoints, textFile); + assertTrue(textFile.exists()); + + List expectedEncodedCheckpoints = new ArrayList<>(); + expectedEncodedCheckpoints.addAll(CHECKPOINTS_12_BYTES_CHAINWORK_ENCODED); + expectedEncodedCheckpoints.addAll(CHECKPOINTS_32_BYTES_CHAINWORK_ENCODED); + assertCheckpointFileContent(expectedEncodedCheckpoints); + + try (FileInputStream checkpointsStream = new FileInputStream(textFile)) { + CheckpointManager checkpointManager = new CheckpointManager(MAINNET, checkpointsStream); + assertEquals(checkpoints.size(), checkpointManager.numCheckpoints()); + } catch (Exception ex) { + fail(); + } + } +} \ No newline at end of file From 2434594fc2a26f506e63d142d97973ad9fdff860 Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Mon, 18 Nov 2024 11:36:46 -0400 Subject: [PATCH 16/23] - Renamed V1 format class variables - Refactored serializeCompactLegacy and serializeCompactV2 to get rid of duplicate logic --- .../org/bitcoinj/core/CheckpointManager.java | 6 +-- .../java/org/bitcoinj/core/StoredBlock.java | 42 +++++++++---------- .../org/bitcoinj/store/LevelDBBlockStore.java | 2 +- .../org/bitcoinj/store/SPVBlockStore.java | 2 +- .../bitcoinj/core/CheckpointManagerTest.java | 12 +++--- .../org/bitcoinj/core/StoredBlockTest.java | 2 +- .../org/bitcoinj/tools/BuildCheckpoints.java | 2 +- .../bitcoinj/tools/BuildCheckpointsTest.java | 10 ++--- 8 files changed, 39 insertions(+), 39 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/CheckpointManager.java b/core/src/main/java/org/bitcoinj/core/CheckpointManager.java index 81d17da56a7..d0123780d1a 100644 --- a/core/src/main/java/org/bitcoinj/core/CheckpointManager.java +++ b/core/src/main/java/org/bitcoinj/core/CheckpointManager.java @@ -137,7 +137,7 @@ private Sha256Hash readBinary(InputStream inputStream) throws IOException { digestInputStream.on(true); int numCheckpoints = dis.readInt(); checkState(numCheckpoints > 0); - final int size = StoredBlock.COMPACT_SERIALIZED_SIZE; + final int size = StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY; ByteBuffer buffer = ByteBuffer.allocate(size); for (int i = 0; i < numCheckpoints; i++) { if (dis.read(buffer.array(), 0, size) < size) @@ -150,7 +150,7 @@ private Sha256Hash readBinary(InputStream inputStream) throws IOException { int actualCheckpointsSize = dis.available(); int expectedCheckpointsSize = numCheckpoints * size; // Check if there are any bytes left in the stream. If it does, it means that checkpoints are malformed - if (dis.available() > 0) { + if (actualCheckpointsSize > 0) { String message = String.format( "Checkpoints size did not match size for version 1 format. Expected checkpoints %d with size of %d bytes, but actual size was %d.", numCheckpoints, expectedCheckpointsSize, actualCheckpointsSize); @@ -189,7 +189,7 @@ private Sha256Hash readTextual(InputStream inputStream) throws IOException { hasher.putBytes(bytes); ByteBuffer buffer = ByteBuffer.wrap(bytes); StoredBlock block; - if (bytes.length == StoredBlock.COMPACT_SERIALIZED_SIZE) { + if (bytes.length == StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY) { block = StoredBlock.deserializeCompactLegacy(params, buffer); } else if (bytes.length == StoredBlock.COMPACT_SERIALIZED_SIZE_V2) { block = StoredBlock.deserializeCompactV2(params, buffer); diff --git a/core/src/main/java/org/bitcoinj/core/StoredBlock.java b/core/src/main/java/org/bitcoinj/core/StoredBlock.java index b9cc40663cd..6c2a9198cb0 100644 --- a/core/src/main/java/org/bitcoinj/core/StoredBlock.java +++ b/core/src/main/java/org/bitcoinj/core/StoredBlock.java @@ -35,16 +35,18 @@ */ public class StoredBlock { - /* Size in bytes to represent the total amount of work done so far on this chain. As of June 22, 2024, it takes 12 - unsigned bytes to store this value, so developers should use the V2 format. */ - private static final int CHAIN_WORK_BYTES_V1 = 12; + /* @deprecated Use {@link #CHAIN_WORK_BYTES_V2} instead. + Size in bytes to represent the total amount of work done so far on this chain. As of June 22, 2024, it takes 12 + unsigned bytes to store this value, so developers should use the V2 format. + */ + private static final int CHAIN_WORK_BYTES_LEGACY = 12; // Size in bytes to represent the total amount of work done so far on this chain. private static final int CHAIN_WORK_BYTES_V2 = 32; // Size in bytes(int) to represent btc block height private static final int HEIGHT_BYTES = 4; // Size in bytes of serialized block in legacy format by {@link #serializeCompactLegacy(ByteBuffer)} - public static final int COMPACT_SERIALIZED_SIZE = Block.HEADER_SIZE + CHAIN_WORK_BYTES_V1 + HEIGHT_BYTES; + public static final int COMPACT_SERIALIZED_SIZE_LEGACY = Block.HEADER_SIZE + CHAIN_WORK_BYTES_LEGACY + HEIGHT_BYTES; // Size in bytes of serialized block in V2 format by {@link #serializeCompactV2(ByteBuffer)} public static final int COMPACT_SERIALIZED_SIZE_V2 = Block.HEADER_SIZE + CHAIN_WORK_BYTES_V2 + HEIGHT_BYTES; @@ -121,7 +123,7 @@ public StoredBlock getPrev(BlockStore store) throws BlockStoreException { } /** - * * @deprecated Use {@link #serializeCompactV2(ByteBuffer)} instead. + * @deprecated Use {@link #serializeCompactV2(ByteBuffer)} instead. * * Serializes the stored block to a custom packed format. Used internally. * As of June 22, 2024, it takes 12 unsigned bytes to store the chain work value, @@ -131,13 +133,8 @@ public StoredBlock getPrev(BlockStore store) throws BlockStoreException { */ @Deprecated public void serializeCompactLegacy(ByteBuffer buffer) { - byte[] chainWorkBytes = Utils.bigIntegerToBytes(getChainWork(), CHAIN_WORK_BYTES_V1); - buffer.put(chainWorkBytes); - buffer.putInt(getHeight()); - // Using unsafeBitcoinSerialize here can give us direct access to the same bytes we read off the wire, - // avoiding serialization round-trips. - byte[] bytes = getHeader().unsafeBitcoinSerialize(); - buffer.put(bytes, 0, Block.HEADER_SIZE); // Trim the trailing 00 byte (zero transactions). + serializeCompact(buffer, CHAIN_WORK_BYTES_LEGACY); + } /** @@ -146,7 +143,11 @@ public void serializeCompactLegacy(ByteBuffer buffer) { * @param buffer buffer to write to */ public void serializeCompactV2(ByteBuffer buffer) { - byte[] chainWorkBytes = Utils.bigIntegerToBytes(getChainWork(), CHAIN_WORK_BYTES_V2); + serializeCompact(buffer, CHAIN_WORK_BYTES_V2); + } + + private void serializeCompact(ByteBuffer buffer, int chainWorkSize) { + byte[] chainWorkBytes = Utils.bigIntegerToBytes(getChainWork(), chainWorkSize); buffer.put(chainWorkBytes); buffer.putInt(getHeight()); // Using unsafeBitcoinSerialize here can give us direct access to the same bytes we read off the wire, @@ -167,13 +168,7 @@ public void serializeCompactV2(ByteBuffer buffer) { */ @Deprecated public static StoredBlock deserializeCompactLegacy(NetworkParameters params, ByteBuffer buffer) throws ProtocolException { - byte[] chainWorkBytes = new byte[StoredBlock.CHAIN_WORK_BYTES_V1]; - buffer.get(chainWorkBytes); - BigInteger chainWork = new BigInteger(1, chainWorkBytes); - int height = buffer.getInt(); // +4 bytes - byte[] header = new byte[Block.HEADER_SIZE + 1]; // Extra byte for the 00 transactions length. - buffer.get(header, 0, Block.HEADER_SIZE); - return new StoredBlock(params.getDefaultSerializer().makeBlock(header), chainWork, height); + return deserializeCompact(params, buffer, StoredBlock.CHAIN_WORK_BYTES_LEGACY); } /** @@ -183,7 +178,12 @@ public static StoredBlock deserializeCompactLegacy(NetworkParameters params, Byt * @return deserialized stored block */ public static StoredBlock deserializeCompactV2(NetworkParameters params, ByteBuffer buffer) throws ProtocolException { - byte[] chainWorkBytes = new byte[StoredBlock.CHAIN_WORK_BYTES_V2]; + return deserializeCompact(params, buffer, StoredBlock.CHAIN_WORK_BYTES_V2); + } + + private static StoredBlock deserializeCompact(NetworkParameters params, ByteBuffer buffer, + int chainWorkSize) { + byte[] chainWorkBytes = new byte[chainWorkSize]; buffer.get(chainWorkBytes); BigInteger chainWork = new BigInteger(1, chainWorkBytes); int height = buffer.getInt(); // +4 bytes diff --git a/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java b/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java index 186a0a39eea..58fff2f1697 100644 --- a/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java @@ -35,7 +35,7 @@ public class LevelDBBlockStore implements BlockStore { private final Context context; private DB db; - private final ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); + private final ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY); private final File path; /** Creates a LevelDB SPV block store using the JNI/C++ version of LevelDB. */ diff --git a/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java b/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java index 74747c92624..94537ed2570 100644 --- a/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java @@ -300,7 +300,7 @@ public NetworkParameters getParams() { return params; } - protected static final int RECORD_SIZE = 32 /* hash */ + StoredBlock.COMPACT_SERIALIZED_SIZE; + protected static final int RECORD_SIZE = 32 /* hash */ + StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY; // File format: // 4 header bytes = "SPVB" diff --git a/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java b/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java index cde4a2480d6..0d90b07f108 100644 --- a/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java +++ b/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java @@ -81,7 +81,7 @@ public void readBinaryCheckpoint_whenMainnet_ok() throws IOException { @Test public void readBinaryCheckpoints_whenCheckpointChainWorkIs12Bytes() throws IOException { List checkpoints = getCheckpoints(CHECKPOINTS_12_BYTES_CHAINWORK_ENCODED, - StoredBlock.COMPACT_SERIALIZED_SIZE); + StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY); try (InputStream binaryCheckpoint = generateBinaryCheckpoints(checkpoints)) { CheckpointManager checkpointManager = new CheckpointManager(MAINNET, binaryCheckpoint); @@ -92,14 +92,14 @@ public void readBinaryCheckpoints_whenCheckpointChainWorkIs12Bytes() throws IOEx private List getCheckpoints(List checkpoints, int blockFormatSize) { ByteBuffer buffer = ByteBuffer.allocate(blockFormatSize); - List decodedCheckpoints = new ArrayList(); + List decodedCheckpoints = new ArrayList<>(); for (String checkpoint : checkpoints) { byte[] bytes = BASE64.decode(checkpoint); buffer.clear(); buffer.put(bytes); buffer.flip(); StoredBlock block; - if (blockFormatSize == StoredBlock.COMPACT_SERIALIZED_SIZE) { + if (blockFormatSize == StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY) { block = StoredBlock.deserializeCompactLegacy(MAINNET, buffer); } else { block = StoredBlock.deserializeCompactV2(MAINNET, buffer); @@ -131,14 +131,14 @@ private InputStream generateBinaryCheckpoints(List checkpoints) { digestStream.on(true); dataStream.writeInt(checkpoints.size()); - ByteBuffer bufferV1 = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); + ByteBuffer bufferV1 = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY); ByteBuffer bufferV2 = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_V2); for (StoredBlock block : checkpoints) { boolean isV1 = block.getChainWork().compareTo(MAX_WORK_V1) <= 0; ByteBuffer buffer = isV1 ? bufferV1 : bufferV2; serializeBlock(buffer, block, isV1); - int limit = isV1 ? StoredBlock.COMPACT_SERIALIZED_SIZE + int limit = isV1 ? StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY : StoredBlock.COMPACT_SERIALIZED_SIZE_V2; dataStream.write(buffer.array(), 0, limit); } @@ -165,7 +165,7 @@ public void readBinaryCheckpoints_whenV2Format_shouldFail() throws IOException { public void readBinaryCheckpoints_whenMixFormats_shouldFail() throws IOException { List checkpointsV1Format = getCheckpoints(CHECKPOINTS_12_BYTES_CHAINWORK_ENCODED, - StoredBlock.COMPACT_SERIALIZED_SIZE); + StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY); List checkpointsV2Format = getCheckpoints(CHECKPOINTS_32_BYTES_CHAINWORK_ENCODED, StoredBlock.COMPACT_SERIALIZED_SIZE_V2); diff --git a/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java b/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java index cfd3ea02655..2a9a5a20108 100644 --- a/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java +++ b/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java @@ -36,7 +36,7 @@ public class StoredBlockTest { private static final int blockHeight = 849137; private static final Block block = bitcoinSerializer.makeBlock(Hex.decode(blockHeader)); - private static final int blockCapacity = StoredBlock.COMPACT_SERIALIZED_SIZE; + private static final int blockCapacity = StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY; private ByteBuffer blockBuffer; @Rule diff --git a/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java b/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java index 26deb5e3903..729abaeebcb 100644 --- a/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java +++ b/tools/src/main/java/org/bitcoinj/tools/BuildCheckpoints.java @@ -175,7 +175,7 @@ protected static void writeTextualCheckpoints(TreeMap chec writer.println("TXT CHECKPOINTS 1"); writer.println("0"); // Number of signatures to read. Do this later. writer.println(checkpoints.size()); - ByteBuffer bufferV1 = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); + ByteBuffer bufferV1 = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY); ByteBuffer bufferV2 = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_V2); for (StoredBlock block : checkpoints.values()) { if (block.getChainWork().compareTo(MAX_WORK_V1) <= 0) { diff --git a/tools/src/test/java/org/bitcoinj/tools/BuildCheckpointsTest.java b/tools/src/test/java/org/bitcoinj/tools/BuildCheckpointsTest.java index b24c4e3170d..934fc92f62d 100644 --- a/tools/src/test/java/org/bitcoinj/tools/BuildCheckpointsTest.java +++ b/tools/src/test/java/org/bitcoinj/tools/BuildCheckpointsTest.java @@ -52,14 +52,14 @@ public class BuildCheckpointsTest { @Before public void setUp() throws Exception { checkpoints = new TreeMap<>(); - textFile = new File("checkpoints.txt"); - textFile.delete(); + textFile = File.createTempFile("checkpoints", ".txt"); + textFile.deleteOnExit(); } @Test public void writeTextualCheckpoints_whenBlocksChainWorkFits12Bytes_shouldBuiltFile() throws IOException { assertFalse(textFile.exists()); - populateCheckpoints(CHECKPOINTS_12_BYTES_CHAINWORK_ENCODED, StoredBlock.COMPACT_SERIALIZED_SIZE, MAINNET); + populateCheckpoints(CHECKPOINTS_12_BYTES_CHAINWORK_ENCODED, StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY, MAINNET); BuildCheckpoints.writeTextualCheckpoints(checkpoints, textFile); assertTrue(textFile.exists()); @@ -111,7 +111,7 @@ private List getCheckpoints(List encodedCheckpoints, int bl buffer.put(bytes); buffer.flip(); StoredBlock block; - if (blockFormatSize == StoredBlock.COMPACT_SERIALIZED_SIZE) { + if (blockFormatSize == StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY) { block = StoredBlock.deserializeCompactLegacy(networkParameters, buffer); } else { block = StoredBlock.deserializeCompactV2(networkParameters, buffer); @@ -141,7 +141,7 @@ public void writeTextualCheckpoints_whenBlocksChainWorkUse32Bytes_shouldBuiltFil @Test public void writeTextualCheckpoints_whenMixBlocksChainWork_shouldBuiltFile() throws IOException { assertFalse(textFile.exists()); - populateCheckpoints(CHECKPOINTS_12_BYTES_CHAINWORK_ENCODED, StoredBlock.COMPACT_SERIALIZED_SIZE, MAINNET); + populateCheckpoints(CHECKPOINTS_12_BYTES_CHAINWORK_ENCODED, StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY, MAINNET); populateCheckpoints(CHECKPOINTS_32_BYTES_CHAINWORK_ENCODED, StoredBlock.COMPACT_SERIALIZED_SIZE_V2, MAINNET); BuildCheckpoints.writeTextualCheckpoints(checkpoints, textFile); assertTrue(textFile.exists()); From ff5419670612f6f770c9d68ce0874af3bd77110a Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Sun, 17 Nov 2024 15:57:41 -0400 Subject: [PATCH 17/23] Add Level DB tests files created using legacy format --- .../leveldb-using-legacy-format/000005.sst | Bin 0 -> 622 bytes .../leveldb-using-legacy-format/000010.sst | Bin 0 -> 622 bytes .../leveldb-using-legacy-format/000011.log | 0 .../resources/leveldb-using-legacy-format/CURRENT | 1 + .../resources/leveldb-using-legacy-format/LOCK | 0 .../resources/leveldb-using-legacy-format/LOG | 5 +++++ .../resources/leveldb-using-legacy-format/LOG.old | 3 +++ .../leveldb-using-legacy-format/MANIFEST-000009 | Bin 0 -> 178 bytes 8 files changed, 9 insertions(+) create mode 100644 core/src/main/resources/leveldb-using-legacy-format/000005.sst create mode 100644 core/src/main/resources/leveldb-using-legacy-format/000010.sst create mode 100644 core/src/main/resources/leveldb-using-legacy-format/000011.log create mode 100644 core/src/main/resources/leveldb-using-legacy-format/CURRENT create mode 100644 core/src/main/resources/leveldb-using-legacy-format/LOCK create mode 100644 core/src/main/resources/leveldb-using-legacy-format/LOG create mode 100644 core/src/main/resources/leveldb-using-legacy-format/LOG.old create mode 100644 core/src/main/resources/leveldb-using-legacy-format/MANIFEST-000009 diff --git a/core/src/main/resources/leveldb-using-legacy-format/000005.sst b/core/src/main/resources/leveldb-using-legacy-format/000005.sst new file mode 100644 index 0000000000000000000000000000000000000000..7077588c3fa43fc139b33536305d665a881bc036 GIT binary patch literal 622 zcmZ3^#>1eIz#zz&z}R@QN~Jw$-_f?4x@$J6{foV6`!-+UA|tz?qAX(xci{$x1&4G4 zA1yJK%jNy*C)}U%y~|^bj_v0sy3Wj^njbu@W>?5Kn7q9;``g(g2jd^^nA2RiES7gs zaQ^i*#k=mMuhEOTm#m=9E^gv5e?O;u0#lc)h)Ihl4~vzweW~7~pjVSum&u=(VdP|B z4Py8&%UHp{UAT$i0fPcl)b`l?i`QPO&ns;Ao^dZyvPINR*oT$XfRXWo$L_jdVP)?H zULMa6tFOLb_xm1i>f-ueH=k?Uner_4oqj$o9%AXw-gsxArK~=}cA_nkk@seJw-?S+ ze|_y@e(d%rCPoehMqfQfh8jkor5hO@>`=d`kpI^9X6!$;O>1;-wjJFU)UHx>vXN0# zr{iJASHJaPw_6WsihpVPFtP6vi2a7g;+{V@OD4$XCIwKI_FSI2w)|IZ*RyGFZ4frEj8nKOV#fg^#@ zES}+C8ADPTg9(G6LUKl8W?n{WVhSS{XT2z6loS&W7rO$Rs0vVnWSR}504I>a!l@0U z6&RT~xmg*r%ot>Vj$mK};o7w^jW7T*I7O{wf Pgz$s#-woX=rS7)@$*k4{ literal 0 HcmV?d00001 diff --git a/core/src/main/resources/leveldb-using-legacy-format/000010.sst b/core/src/main/resources/leveldb-using-legacy-format/000010.sst new file mode 100644 index 0000000000000000000000000000000000000000..690743c366bdc4e0fcab47ef3e5b5815e5f99e71 GIT binary patch literal 622 zcmdnQ%EO?Mz#zz&z}R@QN~Jw$-_f?4x@$J6{foV6`!-+UBBPL?qAX(xci{$x1&4G4 zA1yJK%jNy*C)}U%y~|^bj_v0sy3Wj^ng^_ASI9V+yuCI1+u0)r;~(yr(_FYLmUmHb z{`ED*yY8j0(Tlp5tf0;=ZsIV1zXAsX12?Bt4ySwqQGnYulOfEcKm!J}n+%_s`yVXOP`I42))sy$P&7 z!giu9l9Bgjc()hMQ-6K!Vt(xQC?-Z>Rz*EVh8jko-5VJm>`=d`kpI^9X6!$;O>1;- zwjJFU)UHx>vXN0#=fJ~`uYT*pZnqxN6#vrnVPfAU#xvOqZaGLSyRuK#Ii*{0e>BA6 ziW_NfKo;|ITGg;BCrDn)n8OkEFtu53vj6wwb#9B69W2`XaT{09qkKjIPBmUe21ZsU z9-zk<8Ck0s84g%4e*3qoT4+<%aUHw#vi`*S6Vit}x&n=?=I>bLEf?pdxjNqS|9=Kq z*)`fVAZz$|0}KQek~0!B^D&jzuiuAR+u9{C7jQN~!y8 E0Hv|hEC2ui literal 0 HcmV?d00001 diff --git a/core/src/main/resources/leveldb-using-legacy-format/000011.log b/core/src/main/resources/leveldb-using-legacy-format/000011.log new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/src/main/resources/leveldb-using-legacy-format/CURRENT b/core/src/main/resources/leveldb-using-legacy-format/CURRENT new file mode 100644 index 00000000000..6ba31a31e7d --- /dev/null +++ b/core/src/main/resources/leveldb-using-legacy-format/CURRENT @@ -0,0 +1 @@ +MANIFEST-000009 diff --git a/core/src/main/resources/leveldb-using-legacy-format/LOCK b/core/src/main/resources/leveldb-using-legacy-format/LOCK new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/src/main/resources/leveldb-using-legacy-format/LOG b/core/src/main/resources/leveldb-using-legacy-format/LOG new file mode 100644 index 00000000000..df7038277cf --- /dev/null +++ b/core/src/main/resources/leveldb-using-legacy-format/LOG @@ -0,0 +1,5 @@ +2024/11/17-15:41:04.727721 30711c000 Recovering log #8 +2024/11/17-15:41:04.727754 30711c000 Level-0 table #10: started +2024/11/17-15:41:04.727948 30711c000 Level-0 table #10: 622 bytes OK +2024/11/17-15:41:04.728322 30711c000 Delete type=3 #7 +2024/11/17-15:41:04.728360 30711c000 Delete type=0 #8 diff --git a/core/src/main/resources/leveldb-using-legacy-format/LOG.old b/core/src/main/resources/leveldb-using-legacy-format/LOG.old new file mode 100644 index 00000000000..ba2561df8ae --- /dev/null +++ b/core/src/main/resources/leveldb-using-legacy-format/LOG.old @@ -0,0 +1,3 @@ +2024/11/17-15:41:04.718257 30711c000 Recovering log #6 +2024/11/17-15:41:04.719150 30711c000 Delete type=0 #6 +2024/11/17-15:41:04.719343 30711c000 Delete type=3 #4 diff --git a/core/src/main/resources/leveldb-using-legacy-format/MANIFEST-000009 b/core/src/main/resources/leveldb-using-legacy-format/MANIFEST-000009 new file mode 100644 index 0000000000000000000000000000000000000000..2e9db0ad347a5923a4a01b21deaeeb3542e4bb6a GIT binary patch literal 178 zcmeCom*tFMU}TiaNi9pwNlDUksw_z@&n!-L&d)7KEJ`fNFJfn4eaE7~00WGTC#zK2 zgZ3S5yQ#ZoliI)7o3?NB6)rNeLlg)mXC!9kWuzviFfu_zo-(fp@nB$N;^t&v=3x Date: Sun, 17 Nov 2024 15:58:46 -0400 Subject: [PATCH 18/23] add unit tests for LevelDBBlockStore testing legacy format --- .../bitcoinj/store/LevelDBBlockStoreTest.java | 97 +++++++++++++------ 1 file changed, 68 insertions(+), 29 deletions(-) diff --git a/core/src/test/java/org/bitcoinj/store/LevelDBBlockStoreTest.java b/core/src/test/java/org/bitcoinj/store/LevelDBBlockStoreTest.java index 30aea5e5065..379440bccae 100644 --- a/core/src/test/java/org/bitcoinj/store/LevelDBBlockStoreTest.java +++ b/core/src/test/java/org/bitcoinj/store/LevelDBBlockStoreTest.java @@ -16,12 +16,16 @@ package org.bitcoinj.store; -import org.bitcoinj.core.Address; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.bitcoinj.core.BitcoinSerializer; +import org.bitcoinj.core.Block; import org.bitcoinj.core.Context; -import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.StoredBlock; -import org.bitcoinj.params.*; +import org.bitcoinj.core.Utils; import org.junit.*; import java.io.*; @@ -29,40 +33,75 @@ import static org.junit.Assert.assertEquals; public class LevelDBBlockStoreTest { - private static final NetworkParameters UNITTEST = UnitTestParams.get(); + private static final NetworkParameters MAINNET = NetworkParameters.fromID(NetworkParameters.ID_MAINNET); - @Test - public void basics() throws Exception { - File f = File.createTempFile("leveldbblockstore", null); - f.delete(); + // Max chain work to fit in 12 bytes + private static final BigInteger MAX_WORK_V1 = new BigInteger(/* 12 bytes */ "ffffffffffffffffffffffff", 16); + // Chain work too large to fit in 12 bytes + private static final BigInteger TOO_LARGE_WORK_V1 = new BigInteger(/* 13 bytes */ "ffffffffffffffffffffffffff", 16); + // Max chain work to fit in 32 bytes + private static final BigInteger MAX_WORK_V2 = new BigInteger(/* 32 bytes */ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16); + // Chain work too large to fit in 32 bytes + private static final BigInteger TOO_LARGE_WORK_V2 = new BigInteger(/* 33 bytes */ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16); + + private static final List storedBlockHeadersInHex = Arrays.asList( + // 775000 + "00a0c22d51e2a4331e6d0df54e138f64f78a48ac2c3df3e42d43030000000000000000003a9b781c4034edda9bf6cdc4c15fe1b89c8371a65d0da2536fd7ac73bade67ac2e5ade632027071734409fbf", + // 775001 + "00e0b827d1206fed3dd95dfe26b2ac2dd986c5be5287247ac98101000000000000000000e188f54eaf56db85c22917f482f0918ed201cc6ba0da4018a6d4be1d43648b11bf5bde632027071778d866ec", + // 775002 + "00e00020025ab75d6fd1d6eb279e71874b98de591984153e134c05000000000000000000bb7e5313234ba04a48e7c327abd03efbde0d65a37ffad9e72b3e350ca54d97cf665fde6320270717f98d5f43" + ); - Context context = new Context(UNITTEST); - LevelDBBlockStore store = new LevelDBBlockStore(context, f); - store.reset(); + private static final List blockHeadersInHex = Arrays.asList( + // 775003 + "000000201ccf1f76ab93ea52e22e753f1b3a040c498434141d8a020000000000000000003483c0701c22fbe12e335888ecebf204742d2821c624f0aef02e412d334b9caf9a5fde6320270717edcb6a1b", + // 775004 + "000080209575aaeb1f5a2eacc45d96adf3f58f7e77cc3c3b0e8000000000000000000000e4d3d5a59b650595a1b3fc1341c61db52f7bc61e2ed06ee3b0061f0860c085603960de63202707171797a32b", + // 775005 + "0000652ca44cdbb08c25b45f02e3efd835dc74738807093ab8b80600000000000000000087df28e93fe1ecc6259fe912975af6e8b5e4385385c6c08e51bc0eff42e1886f7261de632027071724d2eab5" + ); - // Check the first block in a new store is the genesis block. - StoredBlock genesis = store.getChainHead(); - assertEquals(UNITTEST.getGenesisBlock(), genesis.getHeader()); - assertEquals(0, genesis.getHeight()); + private static final BitcoinSerializer bitcoinSerializer = new BitcoinSerializer(MAINNET, false); - // Build a new block. - Address to = LegacyAddress.fromBase58(UNITTEST, "mrj2K6txjo2QBcSmuAzHj4nD1oXSEJE1Qo"); - StoredBlock b1 = genesis.build(genesis.getHeader().createNextBlock(to).cloneAsHeader()); - store.put(b1); - store.setChainHead(b1); - store.close(); + @Test + public void testLevelDbBlockStore_whenDbWasCreatedUsingLegacyFormat_shouldWork() throws Exception { + List blockHeaders = getBlockHeaders(storedBlockHeadersInHex); - // Check we can get it back out again if we rebuild the store object. - store = new LevelDBBlockStore(context, f); + String levelDbBlockStorePath = getClass().getResource("/leveldb-using-legacy-format").getPath(); + File levelDbBlockStore = new File(levelDbBlockStorePath); + Context context = new Context(MAINNET); + LevelDBBlockStore store = new LevelDBBlockStore(context, levelDbBlockStore); try { - StoredBlock b2 = store.get(b1.getHeader().getHash()); - assertEquals(b1, b2); - // Check the chain head was stored correctly also. - StoredBlock chainHead = store.getChainHead(); - assertEquals(b1, chainHead); + Block block1 = blockHeaders.get(0); + StoredBlock expectedBlock1 = new StoredBlock(block1.cloneAsHeader(), BigInteger.ZERO, 750000); + StoredBlock actualBlock1 = store.get(block1.getHash()); + assertEquals(expectedBlock1, actualBlock1); + + Block block2 = blockHeaders.get(1); + StoredBlock expectedBlock2 = new StoredBlock(block2.cloneAsHeader(), BigInteger.ONE, 750001); + StoredBlock actualBlock2 = store.get(block2.getHash()); + assertEquals(expectedBlock2, actualBlock2); + + Block block3 = blockHeaders.get(2); + StoredBlock expectedBlock3 = new StoredBlock(block3.cloneAsHeader(), MAX_WORK_V1, 750002); + StoredBlock actualBlock3 = store.get(block3.getHash()); + assertEquals(expectedBlock3, actualBlock3); + + StoredBlock actualChainHead = store.getChainHead(); + assertEquals(expectedBlock3, actualChainHead); } finally { store.close(); - store.destroy(); } } + + private static List getBlockHeaders(List blockHeadersInHex) { + List blocks = new ArrayList<>(); + for (String blockHeader : blockHeadersInHex) { + blocks.add(bitcoinSerializer.makeBlock(Utils.HEX.decode(blockHeader))); + } + return blocks; + } } \ No newline at end of file From ad188deaabf32191bfab112dc6da83cd45c55794 Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Sun, 17 Nov 2024 16:37:44 -0400 Subject: [PATCH 19/23] - add v2 format to LevelDBBlockStore - add unit tests for LevelDBBlockStore testing v2 format --- .../org/bitcoinj/store/LevelDBBlockStore.java | 26 ++- .../000005.sst | Bin 0 -> 622 bytes .../000010.sst | Bin 0 -> 622 bytes .../000011.log | 0 .../CURRENT | 1 + .../LOCK | 0 .../leveldb-using-legacy-format-to-add-v2/LOG | 5 + .../LOG.old | 3 + .../MANIFEST-000009 | Bin 0 -> 178 bytes .../bitcoinj/store/LevelDBBlockStoreTest.java | 186 +++++++++++++++++- 10 files changed, 212 insertions(+), 9 deletions(-) create mode 100644 core/src/main/resources/leveldb-using-legacy-format-to-add-v2/000005.sst create mode 100644 core/src/main/resources/leveldb-using-legacy-format-to-add-v2/000010.sst create mode 100644 core/src/main/resources/leveldb-using-legacy-format-to-add-v2/000011.log create mode 100644 core/src/main/resources/leveldb-using-legacy-format-to-add-v2/CURRENT create mode 100644 core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOCK create mode 100644 core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOG create mode 100644 core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOG.old create mode 100644 core/src/main/resources/leveldb-using-legacy-format-to-add-v2/MANIFEST-000009 diff --git a/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java b/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java index 58fff2f1697..05a1afa0f66 100644 --- a/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java @@ -16,6 +16,7 @@ package org.bitcoinj.store; +import java.math.BigInteger; import org.bitcoinj.core.*; import org.fusesource.leveldbjni.*; import org.iq80.leveldb.*; @@ -35,7 +36,8 @@ public class LevelDBBlockStore implements BlockStore { private final Context context; private DB db; - private final ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY); + private final ByteBuffer legacyBuffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY); + private final ByteBuffer bufferV2 = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_V2); private final File path; /** Creates a LevelDB SPV block store using the JNI/C++ version of LevelDB. */ @@ -76,11 +78,20 @@ private synchronized void initStoreIfNeeded() throws BlockStoreException { setChainHead(storedGenesis); } + private static final BigInteger MAX_WORK_V1 = new BigInteger(/* 12 bytes */ "ffffffffffffffffffffffff", 16); + @Override public synchronized void put(StoredBlock block) throws BlockStoreException { - buffer.clear(); - block.serializeCompactLegacy(buffer); - db.put(block.getHeader().getHash().getBytes(), buffer.array()); + legacyBuffer.clear(); + if (block.getChainWork().compareTo(MAX_WORK_V1) <= 0) { + legacyBuffer.rewind(); + block.serializeCompactLegacy(legacyBuffer); + db.put(block.getHeader().getHash().getBytes(), legacyBuffer.array()); + } else { + bufferV2.rewind(); + block.serializeCompactV2(bufferV2); + db.put(block.getHeader().getHash().getBytes(), bufferV2.array()); + } } @Override @Nullable @@ -88,7 +99,12 @@ public synchronized StoredBlock get(Sha256Hash hash) throws BlockStoreException byte[] bits = db.get(hash.getBytes()); if (bits == null) return null; - return StoredBlock.deserializeCompactLegacy(context.getParams(), ByteBuffer.wrap(bits)); + + if (bits.length == StoredBlock.COMPACT_SERIALIZED_SIZE){ + return StoredBlock.deserializeCompactLegacy(context.getParams(), ByteBuffer.wrap(bits)); + } else { + return StoredBlock.deserializeCompactV2(context.getParams(), ByteBuffer.wrap(bits)); + } } @Override diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/000005.sst b/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/000005.sst new file mode 100644 index 0000000000000000000000000000000000000000..7077588c3fa43fc139b33536305d665a881bc036 GIT binary patch literal 622 zcmZ3^#>1eIz#zz&z}R@QN~Jw$-_f?4x@$J6{foV6`!-+UA|tz?qAX(xci{$x1&4G4 zA1yJK%jNy*C)}U%y~|^bj_v0sy3Wj^njbu@W>?5Kn7q9;``g(g2jd^^nA2RiES7gs zaQ^i*#k=mMuhEOTm#m=9E^gv5e?O;u0#lc)h)Ihl4~vzweW~7~pjVSum&u=(VdP|B z4Py8&%UHp{UAT$i0fPcl)b`l?i`QPO&ns;Ao^dZyvPINR*oT$XfRXWo$L_jdVP)?H zULMa6tFOLb_xm1i>f-ueH=k?Uner_4oqj$o9%AXw-gsxArK~=}cA_nkk@seJw-?S+ ze|_y@e(d%rCPoehMqfQfh8jkor5hO@>`=d`kpI^9X6!$;O>1;-wjJFU)UHx>vXN0# zr{iJASHJaPw_6WsihpVPFtP6vi2a7g;+{V@OD4$XCIwKI_FSI2w)|IZ*RyGFZ4frEj8nKOV#fg^#@ zES}+C8ADPTg9(G6LUKl8W?n{WVhSS{XT2z6loS&W7rO$Rs0vVnWSR}504I>a!l@0U z6&RT~xmg*r%ot>Vj$mK};o7w^jW7T*I7O{wf Pgz$s#-woX=rS7)@$*k4{ literal 0 HcmV?d00001 diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/000010.sst b/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/000010.sst new file mode 100644 index 0000000000000000000000000000000000000000..690743c366bdc4e0fcab47ef3e5b5815e5f99e71 GIT binary patch literal 622 zcmdnQ%EO?Mz#zz&z}R@QN~Jw$-_f?4x@$J6{foV6`!-+UBBPL?qAX(xci{$x1&4G4 zA1yJK%jNy*C)}U%y~|^bj_v0sy3Wj^ng^_ASI9V+yuCI1+u0)r;~(yr(_FYLmUmHb z{`ED*yY8j0(Tlp5tf0;=ZsIV1zXAsX12?Bt4ySwqQGnYulOfEcKm!J}n+%_s`yVXOP`I42))sy$P&7 z!giu9l9Bgjc()hMQ-6K!Vt(xQC?-Z>Rz*EVh8jko-5VJm>`=d`kpI^9X6!$;O>1;- zwjJFU)UHx>vXN0#=fJ~`uYT*pZnqxN6#vrnVPfAU#xvOqZaGLSyRuK#Ii*{0e>BA6 ziW_NfKo;|ITGg;BCrDn)n8OkEFtu53vj6wwb#9B69W2`XaT{09qkKjIPBmUe21ZsU z9-zk<8Ck0s84g%4e*3qoT4+<%aUHw#vi`*S6Vit}x&n=?=I>bLEf?pdxjNqS|9=Kq z*)`fVAZz$|0}KQek~0!B^D&jzuiuAR+u9{C7jQN~!y8 E0Hv|hEC2ui literal 0 HcmV?d00001 diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/000011.log b/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/000011.log new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/CURRENT b/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/CURRENT new file mode 100644 index 00000000000..6ba31a31e7d --- /dev/null +++ b/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/CURRENT @@ -0,0 +1 @@ +MANIFEST-000009 diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOCK b/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOCK new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOG b/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOG new file mode 100644 index 00000000000..df7038277cf --- /dev/null +++ b/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOG @@ -0,0 +1,5 @@ +2024/11/17-15:41:04.727721 30711c000 Recovering log #8 +2024/11/17-15:41:04.727754 30711c000 Level-0 table #10: started +2024/11/17-15:41:04.727948 30711c000 Level-0 table #10: 622 bytes OK +2024/11/17-15:41:04.728322 30711c000 Delete type=3 #7 +2024/11/17-15:41:04.728360 30711c000 Delete type=0 #8 diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOG.old b/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOG.old new file mode 100644 index 00000000000..ba2561df8ae --- /dev/null +++ b/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOG.old @@ -0,0 +1,3 @@ +2024/11/17-15:41:04.718257 30711c000 Recovering log #6 +2024/11/17-15:41:04.719150 30711c000 Delete type=0 #6 +2024/11/17-15:41:04.719343 30711c000 Delete type=3 #4 diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/MANIFEST-000009 b/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/MANIFEST-000009 new file mode 100644 index 0000000000000000000000000000000000000000..2e9db0ad347a5923a4a01b21deaeeb3542e4bb6a GIT binary patch literal 178 zcmeCom*tFMU}TiaNi9pwNlDUksw_z@&n!-L&d)7KEJ`fNFJfn4eaE7~00WGTC#zK2 zgZ3S5yQ#ZoliI)7o3?NB6)rNeLlg)mXC!9kWuzviFfu_zo-(fp@nB$N;^t&v=3x existingBlockHeaders = getBlockHeaders(storedBlockHeadersInHex); + + File levelDbBlockStore = File.createTempFile("leveldb", null); + levelDbBlockStore.delete(); + + Context context = new Context(MAINNET); + LevelDBBlockStore store = new LevelDBBlockStore(context, levelDbBlockStore); + store.reset(); + + Block block1 = existingBlockHeaders.get(0); + StoredBlock expectedBlock1 = new StoredBlock(block1.cloneAsHeader(), BigInteger.ZERO, 750000); + store.put(expectedBlock1); + + Block block2 = existingBlockHeaders.get(1); + StoredBlock expectedBlock2 = new StoredBlock(block2.cloneAsHeader(), BigInteger.ONE, 750001); + store.put(expectedBlock2); + + Block block3 = existingBlockHeaders.get(2); + StoredBlock expectedBlock3 = new StoredBlock(block3.cloneAsHeader(), MAX_WORK_V1, 750002); + store.put(expectedBlock3); + + store.setChainHead(expectedBlock3); + store.close(); + + store = new LevelDBBlockStore(context, levelDbBlockStore); + try { + + StoredBlock actualBlock1 = store.get(block1.getHash()); + assertEquals(expectedBlock1, actualBlock1); + + StoredBlock actualBlock2 = store.get(block2.getHash()); + assertEquals(expectedBlock2, actualBlock2); + + StoredBlock actualBlock3 = store.get(block3.getHash()); + assertEquals(expectedBlock3, actualBlock3); + + StoredBlock actualChainHead = store.getChainHead(); + assertEquals(expectedBlock3, actualChainHead); + } finally { + store.close(); + store.destroy(); + } + } + @Test public void testLevelDbBlockStore_whenDbWasCreatedUsingLegacyFormat_shouldWork() throws Exception { - List blockHeaders = getBlockHeaders(storedBlockHeadersInHex); + List existingBlockHeaders = getBlockHeaders(storedBlockHeadersInHex); String levelDbBlockStorePath = getClass().getResource("/leveldb-using-legacy-format").getPath(); File levelDbBlockStore = new File(levelDbBlockStorePath); Context context = new Context(MAINNET); LevelDBBlockStore store = new LevelDBBlockStore(context, levelDbBlockStore); try { - Block block1 = blockHeaders.get(0); + Block block1 = existingBlockHeaders.get(0); StoredBlock expectedBlock1 = new StoredBlock(block1.cloneAsHeader(), BigInteger.ZERO, 750000); StoredBlock actualBlock1 = store.get(block1.getHash()); assertEquals(expectedBlock1, actualBlock1); - Block block2 = blockHeaders.get(1); + Block block2 = existingBlockHeaders.get(1); StoredBlock expectedBlock2 = new StoredBlock(block2.cloneAsHeader(), BigInteger.ONE, 750001); StoredBlock actualBlock2 = store.get(block2.getHash()); assertEquals(expectedBlock2, actualBlock2); - Block block3 = blockHeaders.get(2); + Block block3 = existingBlockHeaders.get(2); StoredBlock expectedBlock3 = new StoredBlock(block3.cloneAsHeader(), MAX_WORK_V1, 750002); StoredBlock actualBlock3 = store.get(block3.getHash()); assertEquals(expectedBlock3, actualBlock3); @@ -97,6 +143,138 @@ public void testLevelDbBlockStore_whenDbWasCreatedUsingLegacyFormat_shouldWork() } } + @Test + public void testLevelDbBlockStore_whenAddingV2FormatBlocksToExistingDbCreatedUsingLegacyFormat_shouldWork() throws Exception { + List existingBlockHeaders = getBlockHeaders(storedBlockHeadersInHex); + + String levelDbBlockStorePath = getClass().getResource("/leveldb-using-legacy-format-to-add-v2").getPath(); + File levelDbBlockStore = new File(levelDbBlockStorePath); + Context context = new Context(MAINNET); + + LevelDBBlockStore store = new LevelDBBlockStore(context, levelDbBlockStore); + + List blockHeadersToAdd = getBlockHeaders(blockHeadersInHex); + + // Add blocks with V2 format + Block block4 = blockHeadersToAdd.get(0); + StoredBlock expectedBlock4 = new StoredBlock(block4.cloneAsHeader(), TOO_LARGE_WORK_V1, 750003); + store.put(expectedBlock4); + + Block block5 = blockHeadersToAdd.get(1); + StoredBlock expectedBlock5 = new StoredBlock(block5.cloneAsHeader(), MAX_WORK_V2, 750004); + store.put(expectedBlock5); + + Block block6 = blockHeadersToAdd.get(2); + StoredBlock expectedBlock6 = new StoredBlock(block6.cloneAsHeader(), MAX_WORK_V2, 750005); + store.put(expectedBlock6); + + // Set new chain head + store.setChainHead(expectedBlock6); + store.close(); + + store = new LevelDBBlockStore(context, levelDbBlockStore); + try { + Block block1 = existingBlockHeaders.get(0); + StoredBlock expectedBlock1 = new StoredBlock(block1.cloneAsHeader(), BigInteger.ZERO, 750000); + StoredBlock actualBlock1 = store.get(block1.getHash()); + assertEquals(expectedBlock1, actualBlock1); + + Block block2 = existingBlockHeaders.get(1); + StoredBlock expectedBlock2 = new StoredBlock(block2.cloneAsHeader(), BigInteger.ONE, 750001); + StoredBlock actualBlock2 = store.get(block2.getHash()); + assertEquals(expectedBlock2, actualBlock2); + + Block block3 = existingBlockHeaders.get(2); + StoredBlock expectedBlock3 = new StoredBlock(block3.cloneAsHeader(), MAX_WORK_V1, 750002); + StoredBlock actualBlock3 = store.get(block3.getHash()); + assertEquals(expectedBlock3, actualBlock3); + + StoredBlock actualBlock4 = store.get(block4.getHash()); + assertEquals(expectedBlock4, actualBlock4); + + StoredBlock actualBlock5 = store.get(block5.getHash()); + assertEquals(expectedBlock5, actualBlock5); + + StoredBlock actualBlock6 = store.get(block6.getHash()); + assertEquals(expectedBlock6, actualBlock6); + + StoredBlock actualChainHead = store.getChainHead(); + assertEquals(expectedBlock6, actualChainHead); + } finally { + store.close(); + } + } + + @Test + public void testLevelDbBlockStore_whenAddingMixFormatBlocksAndDbIsNew_shouldWork() throws Exception { + List existingBlockHeaders = getBlockHeaders(storedBlockHeadersInHex); + + File levelDbBlockStore = File.createTempFile("leveldb", null); + levelDbBlockStore.delete(); + + Context context = new Context(MAINNET); + LevelDBBlockStore store = new LevelDBBlockStore(context, levelDbBlockStore); + store.reset(); + + Block block1 = existingBlockHeaders.get(0); + StoredBlock expectedBlock1 = new StoredBlock(block1.cloneAsHeader(), BigInteger.ZERO, 750000); + store.put(expectedBlock1); + + Block block2 = existingBlockHeaders.get(1); + StoredBlock expectedBlock2 = new StoredBlock(block2.cloneAsHeader(), BigInteger.ONE, 750001); + store.put(expectedBlock2); + + Block block3 = existingBlockHeaders.get(2); + StoredBlock expectedBlock3 = new StoredBlock(block3.cloneAsHeader(), MAX_WORK_V1, 750002); + store.put(expectedBlock3); + + // Add blocks with V2 format + List blockHeadersToAdd = getBlockHeaders(blockHeadersInHex); + + Block block4 = blockHeadersToAdd.get(0); + StoredBlock expectedBlock4 = new StoredBlock(block4.cloneAsHeader(), TOO_LARGE_WORK_V1, 750003); + store.put(expectedBlock4); + + Block block5 = blockHeadersToAdd.get(1); + StoredBlock expectedBlock5 = new StoredBlock(block5.cloneAsHeader(), MAX_WORK_V2, 750004); + store.put(expectedBlock5); + + Block block6 = blockHeadersToAdd.get(2); + StoredBlock expectedBlock6 = new StoredBlock(block6.cloneAsHeader(), MAX_WORK_V2, 750005); + store.put(expectedBlock6); + + store.setChainHead(expectedBlock6); + store.close(); + + store = new LevelDBBlockStore(context, levelDbBlockStore); + try { + + StoredBlock actualBlock1 = store.get(block1.getHash()); + assertEquals(expectedBlock1, actualBlock1); + + StoredBlock actualBlock2 = store.get(block2.getHash()); + assertEquals(expectedBlock2, actualBlock2); + + StoredBlock actualBlock3 = store.get(block3.getHash()); + assertEquals(expectedBlock3, actualBlock3); + + StoredBlock actualBlock4 = store.get(block4.getHash()); + assertEquals(expectedBlock4, actualBlock4); + + StoredBlock actualBlock5 = store.get(block5.getHash()); + assertEquals(expectedBlock5, actualBlock5); + + StoredBlock actualBlock6 = store.get(block6.getHash()); + assertEquals(expectedBlock6, actualBlock6); + + StoredBlock actualChainHead = store.getChainHead(); + assertEquals(expectedBlock6, actualChainHead); + } finally { + store.close(); + store.destroy(); + } + } + private static List getBlockHeaders(List blockHeadersInHex) { List blocks = new ArrayList<>(); for (String blockHeader : blockHeadersInHex) { From c7208d0f7a1170619efa3a51232bac91ac8c878a Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Mon, 18 Nov 2024 12:14:27 -0400 Subject: [PATCH 20/23] Fix issues after rebased --- core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java b/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java index 05a1afa0f66..654efd0200c 100644 --- a/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/LevelDBBlockStore.java @@ -36,8 +36,10 @@ public class LevelDBBlockStore implements BlockStore { private final Context context; private DB db; + private final ByteBuffer legacyBuffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY); private final ByteBuffer bufferV2 = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_V2); + private final File path; /** Creates a LevelDB SPV block store using the JNI/C++ version of LevelDB. */ @@ -100,7 +102,7 @@ public synchronized StoredBlock get(Sha256Hash hash) throws BlockStoreException if (bits == null) return null; - if (bits.length == StoredBlock.COMPACT_SERIALIZED_SIZE){ + if (bits.length == StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY){ return StoredBlock.deserializeCompactLegacy(context.getParams(), ByteBuffer.wrap(bits)); } else { return StoredBlock.deserializeCompactV2(context.getParams(), ByteBuffer.wrap(bits)); From fec443f072c4450c29034513513b0c32282f259f Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Thu, 21 Nov 2024 08:15:37 -0400 Subject: [PATCH 21/23] Moved test level db files to test/resources --- .../org/bitcoinj/store/LevelDBBlockStoreTest.java | 3 ++- .../000005.sst | Bin .../000010.sst | Bin .../000011.log | 0 .../leveldb-using-legacy-format-to-add-v2/CURRENT | 0 .../leveldb-using-legacy-format-to-add-v2/LOCK | 0 .../leveldb-using-legacy-format-to-add-v2/LOG | 0 .../leveldb-using-legacy-format-to-add-v2/LOG.old | 0 .../MANIFEST-000009 | Bin .../leveldb-using-legacy-format/000005.sst | Bin .../leveldb-using-legacy-format/000010.sst | Bin .../leveldb-using-legacy-format/000011.log | 0 .../resources/leveldb-using-legacy-format/CURRENT | 0 .../resources/leveldb-using-legacy-format/LOCK | 0 .../resources/leveldb-using-legacy-format/LOG | 0 .../resources/leveldb-using-legacy-format/LOG.old | 0 .../leveldb-using-legacy-format/MANIFEST-000009 | Bin 17 files changed, 2 insertions(+), 1 deletion(-) rename core/src/{main => test}/resources/leveldb-using-legacy-format-to-add-v2/000005.sst (100%) rename core/src/{main => test}/resources/leveldb-using-legacy-format-to-add-v2/000010.sst (100%) rename core/src/{main => test}/resources/leveldb-using-legacy-format-to-add-v2/000011.log (100%) rename core/src/{main => test}/resources/leveldb-using-legacy-format-to-add-v2/CURRENT (100%) rename core/src/{main => test}/resources/leveldb-using-legacy-format-to-add-v2/LOCK (100%) rename core/src/{main => test}/resources/leveldb-using-legacy-format-to-add-v2/LOG (100%) rename core/src/{main => test}/resources/leveldb-using-legacy-format-to-add-v2/LOG.old (100%) rename core/src/{main => test}/resources/leveldb-using-legacy-format-to-add-v2/MANIFEST-000009 (100%) rename core/src/{main => test}/resources/leveldb-using-legacy-format/000005.sst (100%) rename core/src/{main => test}/resources/leveldb-using-legacy-format/000010.sst (100%) rename core/src/{main => test}/resources/leveldb-using-legacy-format/000011.log (100%) rename core/src/{main => test}/resources/leveldb-using-legacy-format/CURRENT (100%) rename core/src/{main => test}/resources/leveldb-using-legacy-format/LOCK (100%) rename core/src/{main => test}/resources/leveldb-using-legacy-format/LOG (100%) rename core/src/{main => test}/resources/leveldb-using-legacy-format/LOG.old (100%) rename core/src/{main => test}/resources/leveldb-using-legacy-format/MANIFEST-000009 (100%) diff --git a/core/src/test/java/org/bitcoinj/store/LevelDBBlockStoreTest.java b/core/src/test/java/org/bitcoinj/store/LevelDBBlockStoreTest.java index edac95ea360..5d1a5f821a5 100644 --- a/core/src/test/java/org/bitcoinj/store/LevelDBBlockStoreTest.java +++ b/core/src/test/java/org/bitcoinj/store/LevelDBBlockStoreTest.java @@ -147,7 +147,8 @@ public void testLevelDbBlockStore_whenDbWasCreatedUsingLegacyFormat_shouldWork() public void testLevelDbBlockStore_whenAddingV2FormatBlocksToExistingDbCreatedUsingLegacyFormat_shouldWork() throws Exception { List existingBlockHeaders = getBlockHeaders(storedBlockHeadersInHex); - String levelDbBlockStorePath = getClass().getResource("/leveldb-using-legacy-format-to-add-v2").getPath(); + String levelDbBlockStorePath = getClass().getResource( + "/leveldb-using-legacy-format-to-add-v2").getPath(); File levelDbBlockStore = new File(levelDbBlockStorePath); Context context = new Context(MAINNET); diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/000005.sst b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/000005.sst similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format-to-add-v2/000005.sst rename to core/src/test/resources/leveldb-using-legacy-format-to-add-v2/000005.sst diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/000010.sst b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/000010.sst similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format-to-add-v2/000010.sst rename to core/src/test/resources/leveldb-using-legacy-format-to-add-v2/000010.sst diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/000011.log b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/000011.log similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format-to-add-v2/000011.log rename to core/src/test/resources/leveldb-using-legacy-format-to-add-v2/000011.log diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/CURRENT b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/CURRENT similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format-to-add-v2/CURRENT rename to core/src/test/resources/leveldb-using-legacy-format-to-add-v2/CURRENT diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOCK b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/LOCK similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOCK rename to core/src/test/resources/leveldb-using-legacy-format-to-add-v2/LOCK diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOG b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/LOG similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOG rename to core/src/test/resources/leveldb-using-legacy-format-to-add-v2/LOG diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOG.old b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/LOG.old similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format-to-add-v2/LOG.old rename to core/src/test/resources/leveldb-using-legacy-format-to-add-v2/LOG.old diff --git a/core/src/main/resources/leveldb-using-legacy-format-to-add-v2/MANIFEST-000009 b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/MANIFEST-000009 similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format-to-add-v2/MANIFEST-000009 rename to core/src/test/resources/leveldb-using-legacy-format-to-add-v2/MANIFEST-000009 diff --git a/core/src/main/resources/leveldb-using-legacy-format/000005.sst b/core/src/test/resources/leveldb-using-legacy-format/000005.sst similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format/000005.sst rename to core/src/test/resources/leveldb-using-legacy-format/000005.sst diff --git a/core/src/main/resources/leveldb-using-legacy-format/000010.sst b/core/src/test/resources/leveldb-using-legacy-format/000010.sst similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format/000010.sst rename to core/src/test/resources/leveldb-using-legacy-format/000010.sst diff --git a/core/src/main/resources/leveldb-using-legacy-format/000011.log b/core/src/test/resources/leveldb-using-legacy-format/000011.log similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format/000011.log rename to core/src/test/resources/leveldb-using-legacy-format/000011.log diff --git a/core/src/main/resources/leveldb-using-legacy-format/CURRENT b/core/src/test/resources/leveldb-using-legacy-format/CURRENT similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format/CURRENT rename to core/src/test/resources/leveldb-using-legacy-format/CURRENT diff --git a/core/src/main/resources/leveldb-using-legacy-format/LOCK b/core/src/test/resources/leveldb-using-legacy-format/LOCK similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format/LOCK rename to core/src/test/resources/leveldb-using-legacy-format/LOCK diff --git a/core/src/main/resources/leveldb-using-legacy-format/LOG b/core/src/test/resources/leveldb-using-legacy-format/LOG similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format/LOG rename to core/src/test/resources/leveldb-using-legacy-format/LOG diff --git a/core/src/main/resources/leveldb-using-legacy-format/LOG.old b/core/src/test/resources/leveldb-using-legacy-format/LOG.old similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format/LOG.old rename to core/src/test/resources/leveldb-using-legacy-format/LOG.old diff --git a/core/src/main/resources/leveldb-using-legacy-format/MANIFEST-000009 b/core/src/test/resources/leveldb-using-legacy-format/MANIFEST-000009 similarity index 100% rename from core/src/main/resources/leveldb-using-legacy-format/MANIFEST-000009 rename to core/src/test/resources/leveldb-using-legacy-format/MANIFEST-000009 From 66389679131703d88304af1594908774484bc61c Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Wed, 27 Nov 2024 19:29:46 -0400 Subject: [PATCH 22/23] Add delete call to guarantee file is deleted on each test execution --- .../java/org/bitcoinj/tools/BuildCheckpointsTest.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/src/test/java/org/bitcoinj/tools/BuildCheckpointsTest.java b/tools/src/test/java/org/bitcoinj/tools/BuildCheckpointsTest.java index 934fc92f62d..726bb0eabd6 100644 --- a/tools/src/test/java/org/bitcoinj/tools/BuildCheckpointsTest.java +++ b/tools/src/test/java/org/bitcoinj/tools/BuildCheckpointsTest.java @@ -19,6 +19,7 @@ import org.bitcoinj.core.CheckpointManager; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.StoredBlock; +import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -53,7 +54,12 @@ public class BuildCheckpointsTest { public void setUp() throws Exception { checkpoints = new TreeMap<>(); textFile = File.createTempFile("checkpoints", ".txt"); - textFile.deleteOnExit(); + textFile.delete(); + } + + @After + public void tearDown() { + textFile.delete(); } @Test From 8f6a259c83e68d1da0bdb91e448b07890caddc11 Mon Sep 17 00:00:00 2001 From: nathanieliov Date: Wed, 4 Dec 2024 10:25:18 -0400 Subject: [PATCH 23/23] Bump version to 0.15.6-rsk-4-SNAPSHOT --- core/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/build.gradle b/core/build.gradle index 502277d8050..885eb36dae5 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'com.google.protobuf' apply plugin: 'maven' apply plugin: 'eclipse' -version = '0.15.6-rsk-3' +version = '0.15.6-rsk-4-SNAPSHOT' archivesBaseName = 'bitcoinj-core' eclipse.project.name = 'bitcoinj-core'