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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