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