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'
diff --git a/core/src/main/java/org/bitcoinj/core/CheckpointManager.java b/core/src/main/java/org/bitcoinj/core/CheckpointManager.java
index 8b8db387dfd..d0123780d1a 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,11 @@ 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. */
+ @Deprecated
private Sha256Hash readBinary(InputStream inputStream) throws IOException {
DataInputStream dis = null;
try {
@@ -132,15 +137,26 @@ 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)
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);
}
+
+ 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 (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);
+ 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);
@@ -168,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.deserializeCompact(params, buffer);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes);
+ StoredBlock block;
+ 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);
+ } 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/main/java/org/bitcoinj/core/StoredBlock.java b/core/src/main/java/org/bitcoinj/core/StoredBlock.java
index ed771c63f3d..6c2a9198cb0 100644
--- a/core/src/main/java/org/bitcoinj/core/StoredBlock.java
+++ b/core/src/main/java/org/bitcoinj/core/StoredBlock.java
@@ -35,11 +35,20 @@
*/
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
+ /* @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_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;
private Block header;
private BigInteger chainWork;
@@ -113,13 +122,32 @@ 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}. */
- public void serializeCompact(ByteBuffer buffer) {
- byte[] chainWorkBytes = Utils.bigIntegerToBytes(getChainWork(), CHAIN_WORK_BYTES);
- if (chainWorkBytes.length < CHAIN_WORK_BYTES) {
- // Pad to the right size.
- buffer.put(EMPTY_BYTES, 0, CHAIN_WORK_BYTES - chainWorkBytes.length);
- }
+ /**
+ * @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
+ */
+ @Deprecated
+ public void serializeCompactLegacy(ByteBuffer buffer) {
+ serializeCompact(buffer, CHAIN_WORK_BYTES_LEGACY);
+
+ }
+
+ /**
+ * Serializes the stored block to a custom packed format. Used internally.
+ *
+ * @param buffer buffer to write to
+ */
+ public void serializeCompactV2(ByteBuffer buffer) {
+ 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,
@@ -128,9 +156,34 @@ public void serializeCompact(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}. */
- public static StoredBlock deserializeCompact(NetworkParameters params, ByteBuffer buffer) throws ProtocolException {
- byte[] chainWorkBytes = new byte[StoredBlock.CHAIN_WORK_BYTES];
+ /**
+ * @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.
+ *
+ * @param buffer data to deserialize
+ * @return deserialized stored block
+ */
+ @Deprecated
+ public static StoredBlock deserializeCompactLegacy(NetworkParameters params, ByteBuffer buffer) throws ProtocolException {
+ return deserializeCompact(params, buffer, StoredBlock.CHAIN_WORK_BYTES_LEGACY);
+ }
+
+ /**
+ * 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 {
+ 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 ba3d8255903..654efd0200c 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,10 @@ 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 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 +80,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.serializeCompact(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 +101,12 @@ 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));
+
+ 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));
+ }
}
@Override
diff --git a/core/src/main/java/org/bitcoinj/store/LevelDBFullPrunedBlockStore.java b/core/src/main/java/org/bitcoinj/store/LevelDBFullPrunedBlockStore.java
index 814f953808d..e9d5be78a63 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)
@@ -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 298dbaf7482..94537ed2570 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(); }
@@ -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;
}
@@ -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 2690bf29261..0d90b07f108 100644
--- a/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java
+++ b/core/src/test/java/org/bitcoinj/core/CheckpointManagerTest.java
@@ -16,51 +16,204 @@
package org.bitcoinj.core;
-import org.easymock.EasyMockRunner;
-import org.easymock.Mock;
+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.junit.Assert;
import org.junit.Test;
-import org.junit.runner.RunWith;
import java.io.IOException;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
+import static org.bitcoinj.core.CheckpointManager.BASE64;
-@RunWith(EasyMockRunner.class)
public class CheckpointManagerTest {
- @Mock
- NetworkParameters params;
+ 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"
+ );
+
+ @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_LEGACY);
+ 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_LEGACY) {
+ block = StoredBlock.deserializeCompactLegacy(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.serializeCompactLegacy(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_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_LEGACY
+ : 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_LEGACY);
+ 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);
+ }
+ }
@Test(expected = NullPointerException.class)
public void shouldThrowNullPointerExceptionWhenCheckpointsNotFound() throws IOException {
- expect(params.getId()).andReturn("org/bitcoinj/core/checkpointmanagertest/notFound");
- replay(params);
- new CheckpointManager(params, null);
+ InputStream checkpointStream = getClass().getResourceAsStream("/org/bitcoinj/core/checkpointmanagertest/notFound.checkpoints.txt");
+ new CheckpointManager(null, checkpointStream);
}
@Test(expected = IOException.class)
public void shouldThrowNullPointerExceptionWhenCheckpointsInUnknownFormat() throws IOException {
- expect(params.getId()).andReturn("org/bitcoinj/core/checkpointmanagertest/unsupportedFormat");
- replay(params);
- new CheckpointManager(params, null);
+ InputStream checkpointStream = getClass().getResourceAsStream("/org/bitcoinj/core/checkpointmanagertest/unsupportedFormat.checkpoints.txt");
+ new CheckpointManager(MAINNET, checkpointStream);
}
@Test(expected = IllegalStateException.class)
public void shouldThrowIllegalStateExceptionWithNoCheckpoints() throws IOException {
- expect(params.getId()).andReturn("org/bitcoinj/core/checkpointmanagertest/noCheckpoints");
- replay(params);
- new CheckpointManager(params, null);
+ InputStream checkpointStream = getClass().getResourceAsStream("/org/bitcoinj/core/checkpointmanagertest/noCheckpoints.checkpoints.txt");
+ new CheckpointManager(MAINNET, checkpointStream);
}
@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);
+ 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/java/org/bitcoinj/core/StoredBlockTest.java b/core/src/test/java/org/bitcoinj/core/StoredBlockTest.java
index c743aab4081..2a9a5a20108 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 static final int blockCapacity = StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY;
private ByteBuffer blockBuffer;
+ @Rule
+ public ExpectedException exceptionRule = ExpectedException.none();
+
@Before
public void setUp() {
blockBuffer = ByteBuffer.allocate(blockCapacity);
@@ -26,81 +49,164 @@ 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 serializeCompactLegacy_forNegativeChainWork_throwsException() {
+
+ StoredBlock blockToStore = new StoredBlock(block, NEGATIVE_CHAIN_WORK, blockHeight);
+
+ // serialize block should throw illegal argument exception
+ blockToStore.serializeCompactLegacy(blockBuffer);
+ }
+
@Test
- public void serializeAndDeserializeCompact_forZeroChainWork_works() {
- BigInteger chainWork = BigInteger.ZERO;
- StoredBlock blockToStore = new StoredBlock(block, chainWork, blockHeight);
+ 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
blockBuffer.rewind();
- StoredBlock blockDeserialized = StoredBlock.deserializeCompact(mainnet, blockBuffer);
+ StoredBlock blockDeserialized = StoredBlock.deserializeCompactLegacy(mainnet, blockBuffer);
assertEquals(blockDeserialized, blockToStore);
}
@Test
- public void serializeAndDeserializeCompact_forSmallChainWork_works() {
- BigInteger chainWork = BigInteger.ONE;
- StoredBlock blockToStore = new StoredBlock(block, chainWork, blockHeight);
+ 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
blockBuffer.rewind();
- StoredBlock blockDeserialized = StoredBlock.deserializeCompact(mainnet, blockBuffer);
+ StoredBlock blockDeserialized = StoredBlock.deserializeCompactLegacy(mainnet, blockBuffer);
assertEquals(blockDeserialized, blockToStore);
}
@Test
- public void serializeAndDeserializeCompact_for8bytesChainWork_works() {
- BigInteger chainWork = new BigInteger("ffffffffffffffff", 16); // 8 bytes
- StoredBlock blockToStore = new StoredBlock(block, chainWork, blockHeight);
+ 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
blockBuffer.rewind();
- StoredBlock blockDeserialized = StoredBlock.deserializeCompact(mainnet, blockBuffer);
+ StoredBlock blockDeserialized = StoredBlock.deserializeCompactLegacy(mainnet, blockBuffer);
assertEquals(blockDeserialized, blockToStore);
}
@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);
+ 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
blockBuffer.rewind();
- StoredBlock blockDeserialized = StoredBlock.deserializeCompact(mainnet, blockBuffer);
+ StoredBlock blockDeserialized = StoredBlock.deserializeCompactLegacy(mainnet, blockBuffer);
assertEquals(blockDeserialized, blockToStore);
}
@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);
+ public void serializeCompactLegacy_for13BytesChainWork_throwsException() {
+ StoredBlock blockToStore = new StoredBlock(block, TOO_LARGE_WORK_V1, blockHeight);
+
+ // serialize block should throw illegal argument exception
+ blockToStore.serializeCompactLegacy(blockBuffer);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void serializeCompactLegacy_forMoreThan32BytesChainWork_throwsException() {
+ StoredBlock blockToStore = new StoredBlock(block, TOO_LARGE_WORK_V2, blockHeight);
+
+ // serialize block should throw illegal argument exception
+ blockToStore.serializeCompactLegacy(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.serializeCompact(blockBuffer);
+ blockToStore.serializeCompactV2(blockBuffer);
+ }
+
+ @Test
+ public void serializeAndDeserializeCompactV2_forZeroChainWork_works() {
+ testSerializeAndDeserializeCompactV2(ZERO_CHAIN_WORK);
+ }
+
+ @Test
+ public void serializeAndDeserializeCompactV2_forSmallChainWork_works() {
+ testSerializeAndDeserializeCompactV2(SMALL_CHAIN_WORK);
+ }
+
+ @Test
+ public void serializeAndDeserializeCompactV2_for8BytesChainWork_works() {
+ testSerializeAndDeserializeCompactV2(EIGHT_BYTES_WORK_V1);
+ }
+
+ @Test
+ public void serializeAndDeserializeCompactV2_for12ByteChainWork_works() {
+ testSerializeAndDeserializeCompactV2(MAX_WORK_V1);
+ }
+
+ @Test
+ public void serializeAndDeserializeCompactV2_forTooLargeWorkV1_works() {
+ testSerializeAndDeserializeCompactV2(TOO_LARGE_WORK_V1);
+ }
+
+ @Test
+ public void serializeAndDeserializeCompactV2_for32BytesChainWork_works() {
+ testSerializeAndDeserializeCompactV2(MAX_WORK_V2);
+ }
+
+ 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
+ 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));
}
}
diff --git a/core/src/test/java/org/bitcoinj/store/LevelDBBlockStoreTest.java b/core/src/test/java/org/bitcoinj/store/LevelDBBlockStoreTest.java
index 30aea5e5065..5d1a5f821a5 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,254 @@
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);
+
+ // 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"
+ );
+
+ private static final List blockHeadersInHex = Arrays.asList(
+ // 775003
+ "000000201ccf1f76ab93ea52e22e753f1b3a040c498434141d8a020000000000000000003483c0701c22fbe12e335888ecebf204742d2821c624f0aef02e412d334b9caf9a5fde6320270717edcb6a1b",
+ // 775004
+ "000080209575aaeb1f5a2eacc45d96adf3f58f7e77cc3c3b0e8000000000000000000000e4d3d5a59b650595a1b3fc1341c61db52f7bc61e2ed06ee3b0061f0860c085603960de63202707171797a32b",
+ // 775005
+ "0000652ca44cdbb08c25b45f02e3efd835dc74738807093ab8b80600000000000000000087df28e93fe1ecc6259fe912975af6e8b5e4385385c6c08e51bc0eff42e1886f7261de632027071724d2eab5"
+ );
+
+ private static final BitcoinSerializer bitcoinSerializer = new BitcoinSerializer(MAINNET, false);
@Test
- public void basics() throws Exception {
- File f = File.createTempFile("leveldbblockstore", null);
- f.delete();
+ public void testLevelDbBlockStore_whenDbIsNew_shouldWork() throws Exception {
+ List existingBlockHeaders = getBlockHeaders(storedBlockHeadersInHex);
+
+ File levelDbBlockStore = File.createTempFile("leveldb", null);
+ levelDbBlockStore.delete();
- Context context = new Context(UNITTEST);
- LevelDBBlockStore store = new LevelDBBlockStore(context, f);
+ Context context = new Context(MAINNET);
+ LevelDBBlockStore store = new LevelDBBlockStore(context, levelDbBlockStore);
store.reset();
- // 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());
+ Block block1 = existingBlockHeaders.get(0);
+ StoredBlock expectedBlock1 = new StoredBlock(block1.cloneAsHeader(), BigInteger.ZERO, 750000);
+ store.put(expectedBlock1);
- // 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);
+ 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();
- // Check we can get it back out again if we rebuild the store object.
- store = new LevelDBBlockStore(context, f);
+ 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);
+
+ 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 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 = 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 actualChainHead = store.getChainHead();
+ assertEquals(expectedBlock3, actualChainHead);
+ } finally {
+ store.close();
+ }
+ }
+
+ @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) {
+ blocks.add(bitcoinSerializer.makeBlock(Utils.HEX.decode(blockHeader)));
+ }
+ return blocks;
+ }
}
\ No newline at end of file
diff --git a/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/000005.sst b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/000005.sst
new file mode 100644
index 00000000000..7077588c3fa
Binary files /dev/null and b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/000005.sst differ
diff --git a/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/000010.sst b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/000010.sst
new file mode 100644
index 00000000000..690743c366b
Binary files /dev/null and b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/000010.sst differ
diff --git a/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/000011.log b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/000011.log
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/CURRENT b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/CURRENT
new file mode 100644
index 00000000000..6ba31a31e7d
--- /dev/null
+++ b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/CURRENT
@@ -0,0 +1 @@
+MANIFEST-000009
diff --git a/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/LOCK b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/LOCK
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/LOG b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/LOG
new file mode 100644
index 00000000000..df7038277cf
--- /dev/null
+++ b/core/src/test/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/test/resources/leveldb-using-legacy-format-to-add-v2/LOG.old b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/LOG.old
new file mode 100644
index 00000000000..ba2561df8ae
--- /dev/null
+++ b/core/src/test/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/test/resources/leveldb-using-legacy-format-to-add-v2/MANIFEST-000009 b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/MANIFEST-000009
new file mode 100644
index 00000000000..2e9db0ad347
Binary files /dev/null and b/core/src/test/resources/leveldb-using-legacy-format-to-add-v2/MANIFEST-000009 differ
diff --git a/core/src/test/resources/leveldb-using-legacy-format/000005.sst b/core/src/test/resources/leveldb-using-legacy-format/000005.sst
new file mode 100644
index 00000000000..7077588c3fa
Binary files /dev/null and b/core/src/test/resources/leveldb-using-legacy-format/000005.sst differ
diff --git a/core/src/test/resources/leveldb-using-legacy-format/000010.sst b/core/src/test/resources/leveldb-using-legacy-format/000010.sst
new file mode 100644
index 00000000000..690743c366b
Binary files /dev/null and b/core/src/test/resources/leveldb-using-legacy-format/000010.sst differ
diff --git a/core/src/test/resources/leveldb-using-legacy-format/000011.log b/core/src/test/resources/leveldb-using-legacy-format/000011.log
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/core/src/test/resources/leveldb-using-legacy-format/CURRENT b/core/src/test/resources/leveldb-using-legacy-format/CURRENT
new file mode 100644
index 00000000000..6ba31a31e7d
--- /dev/null
+++ b/core/src/test/resources/leveldb-using-legacy-format/CURRENT
@@ -0,0 +1 @@
+MANIFEST-000009
diff --git a/core/src/test/resources/leveldb-using-legacy-format/LOCK b/core/src/test/resources/leveldb-using-legacy-format/LOCK
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/core/src/test/resources/leveldb-using-legacy-format/LOG b/core/src/test/resources/leveldb-using-legacy-format/LOG
new file mode 100644
index 00000000000..df7038277cf
--- /dev/null
+++ b/core/src/test/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/test/resources/leveldb-using-legacy-format/LOG.old b/core/src/test/resources/leveldb-using-legacy-format/LOG.old
new file mode 100644
index 00000000000..ba2561df8ae
--- /dev/null
+++ b/core/src/test/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/test/resources/leveldb-using-legacy-format/MANIFEST-000009 b/core/src/test/resources/leveldb-using-legacy-format/MANIFEST-000009
new file mode 100644
index 00000000000..2e9db0ad347
Binary files /dev/null and b/core/src/test/resources/leveldb-using-legacy-format/MANIFEST-000009 differ
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/org.bitcoin.production.checkpoints b/core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/org.bitcoin.production.checkpoints
new file mode 100644
index 00000000000..c4a1b1ecfb3
Binary files /dev/null and b/core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/org.bitcoin.production.checkpoints differ
diff --git a/core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/org.bitcoin.test.checkpoints b/core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/org.bitcoin.test.checkpoints
new file mode 100644
index 00000000000..d1aebad1b66
Binary files /dev/null and b/core/src/test/resources/org/bitcoinj/core/checkpointmanagertest/org.bitcoin.test.checkpoints differ
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
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 fb8b8d4b29b..729abaeebcb 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;
@@ -32,10 +34,8 @@
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
-import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
@@ -43,8 +43,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,55 +154,39 @@ 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.serializeCompact(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));
+ 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_LEGACY);
+ ByteBuffer bufferV2 = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_V2);
for (StoredBlock block : checkpoints.values()) {
- block.serializeCompact(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() + "'.");
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..726bb0eabd6
--- /dev/null
+++ b/tools/src/test/java/org/bitcoinj/tools/BuildCheckpointsTest.java
@@ -0,0 +1,167 @@
+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.After;
+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 = File.createTempFile("checkpoints", ".txt");
+ textFile.delete();
+ }
+
+ @After
+ public void tearDown() {
+ textFile.delete();
+ }
+
+ @Test
+ public void writeTextualCheckpoints_whenBlocksChainWorkFits12Bytes_shouldBuiltFile() throws IOException {
+ assertFalse(textFile.exists());
+ populateCheckpoints(CHECKPOINTS_12_BYTES_CHAINWORK_ENCODED, StoredBlock.COMPACT_SERIALIZED_SIZE_LEGACY, 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_LEGACY) {
+ 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_LEGACY, 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