diff --git a/core/src/main/java/com/google/bitcoin/core/CheckpointManager.java b/core/src/main/java/com/google/bitcoin/core/CheckpointManager.java
new file mode 100644
index 000000000..bf1b83a39
--- /dev/null
+++ b/core/src/main/java/com/google/bitcoin/core/CheckpointManager.java
@@ -0,0 +1,125 @@
+/**
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.bitcoin.core;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.TreeMap;
+
+import static com.google.common.base.Preconditions.*;
+
+/**
+ *
Vends hard-coded {@link StoredBlock}s for blocks throughout the chain. Checkpoints serve several purposes:
+ *
+ * - They act as a safety mechanism against huge re-orgs that could rewrite large chunks of history, thus
+ * constraining the block chain to be a consensus mechanism only for recent parts of the timeline.
+ * - They allow synchronization to the head of the chain for new wallets/users much faster than syncing all
+ * headers from the genesis block.
+ * - They mark each BIP30-violating block, which simplifies full verification logic quite significantly. BIP30
+ * handles the case of blocks that contained duplicated coinbase transactions.
+ *
+ *
+ * Checkpoints are used by a {@link BlockChain} to initialize fresh {@link com.google.bitcoin.store.SPVBlockStore}s,
+ * and by {@link FullPrunedBlockChain} to prevent re-orgs beyond them.
+ */
+public class CheckpointManager {
+ private static final Logger log = LoggerFactory.getLogger(CheckpointManager.class);
+
+ // Map of block header time to data.
+ protected final TreeMap checkpoints = new TreeMap();
+
+ protected final NetworkParameters params;
+ protected final Sha256Hash dataHash;
+
+ public CheckpointManager(NetworkParameters params, InputStream inputStream) throws IOException {
+ this.params = checkNotNull(params);
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ DigestInputStream digestInputStream = new DigestInputStream(checkNotNull(inputStream), digest);
+ DataInputStream dis = new DataInputStream(digestInputStream);
+ digestInputStream.on(false);
+ String magic = "CHECKPOINTS 1";
+ byte[] header = new byte[magic.length()];
+ dis.readFully(header);
+ if (!Arrays.equals(header, magic.getBytes("US-ASCII")))
+ throw new IOException("Header bytes did not match expected version");
+ int numSignatures = dis.readInt();
+ for (int i = 0; i < numSignatures; i++) {
+ byte[] sig = new byte[65];
+ dis.readFully(sig);
+ // TODO: Do something with the signature here.
+ }
+ digestInputStream.on(true);
+ int numCheckpoints = dis.readInt();
+ checkState(numCheckpoints > 0);
+ ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
+ for (int i = 0; i < numCheckpoints; i++) {
+ dis.read(buffer.array(), 0, StoredBlock.COMPACT_SERIALIZED_SIZE);
+ StoredBlock block = StoredBlock.deserializeCompact(params, buffer);
+ buffer.position(0);
+ checkpoints.put(block.getHeader().getTimeSeconds(), block);
+ }
+ dataHash = new Sha256Hash(digest.digest());
+ log.info("Read {} checkpoints, hash is {}", checkpoints.size(), dataHash);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e); // Cannot happen.
+ } catch (ProtocolException e) {
+ throw new IOException(e);
+ } finally {
+ inputStream.close();
+ }
+ }
+
+ /**
+ * Returns a {@link StoredBlock} representing the last checkpoint before the given time, for example, normally
+ * you would want to know the checkpoint before the earliest wallet birthday.
+ */
+ public StoredBlock getCheckpointBefore(int time) {
+ checkArgument(time > params.genesisBlock.getTimeSeconds());
+ // This is thread safe because the map never changes after creation.
+ Map.Entry entry = checkpoints.floorEntry((long) time);
+ if (entry == null) {
+ try {
+ Block genesis = params.genesisBlock.cloneAsHeader();
+ return new StoredBlock(genesis, genesis.getWork(), 0);
+ } catch (VerificationException e) {
+ throw new RuntimeException(e); // Cannot happen.
+ }
+ }
+ return entry.getValue();
+ }
+
+ /** Returns the number of checkpoints that were loaded. */
+ public int numCheckpoints() {
+ return checkpoints.size();
+ }
+
+ /** Returns a hash of the concatenated checkpoint data. */
+ public Sha256Hash getDataHash() {
+ return dataHash;
+ }
+}
diff --git a/tools/src/main/java/com/google/bitcoin/tools/BuildCheckpoints.java b/tools/src/main/java/com/google/bitcoin/tools/BuildCheckpoints.java
new file mode 100644
index 000000000..3cb36b025
--- /dev/null
+++ b/tools/src/main/java/com/google/bitcoin/tools/BuildCheckpoints.java
@@ -0,0 +1,86 @@
+package com.google.bitcoin.tools;
+
+import com.google.bitcoin.core.*;
+import com.google.bitcoin.store.BlockStore;
+import com.google.bitcoin.store.MemoryBlockStore;
+import com.google.bitcoin.store.SPVBlockStore;
+import com.google.bitcoin.utils.BriefLogFormatter;
+
+import java.io.*;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.security.DigestOutputStream;
+import java.security.MessageDigest;
+import java.util.Date;
+import java.util.TreeMap;
+
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Downloads and verifies a full chain from your local peer, emitting checkpoints at each difficulty transition period
+ * to a file which is then signed with your key.
+ */
+public class BuildCheckpoints {
+ public static void main(String[] args) throws Exception {
+ BriefLogFormatter.init();
+ final NetworkParameters params = NetworkParameters.prodNet();
+
+ // Sorted map of UNIX time of block to StoredBlock object.
+ final TreeMap checkpoints = new TreeMap();
+
+ final BlockStore store = new MemoryBlockStore(params);
+ final BlockChain chain = new BlockChain(params, store);
+ final PeerGroup peerGroup = new PeerGroup(params, chain);
+ peerGroup.addAddress(InetAddress.getLocalHost());
+ peerGroup.setFastCatchupTimeSecs(new Date().getTime() / 1000);
+
+ chain.addListener(new AbstractBlockChainListener() {
+ @Override
+ public void notifyNewBestBlock(StoredBlock block) throws VerificationException {
+ int height = block.getHeight();
+ if (height % params.interval == 0) {
+ System.out.println(String.format("Checkpointing block %s at height %d",
+ block.getHeader().getHash(), block.getHeight()));
+ checkpoints.put(height, block);
+ }
+ }
+ });
+
+ peerGroup.startAndWait();
+ peerGroup.downloadBlockChain();
+
+ checkState(checkpoints.size() > 0);
+
+ // Write checkpoint data out.
+ final FileOutputStream fileOutputStream = new FileOutputStream("checkpoints", false);
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ 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 = new Sha256Hash(digest.digest());
+ System.out.println("Hash of checkpoints data is " + checkpointsHash);
+ digestOutputStream.close();
+ fileOutputStream.close();
+
+ peerGroup.stopAndWait();
+ store.close();
+
+ // Sanity check the created file.
+ CheckpointManager manager = new CheckpointManager(params, new FileInputStream("checkpoints"));
+ checkState(manager.numCheckpoints() == checkpoints.size());
+ StoredBlock test = manager.getCheckpointBefore(1348310800); // Just after block 200,000
+ checkState(test.getHeight() == 199584);
+ checkState(test.getHeader().getHashAsString().equals("000000000000002e00a243fe9aa49c78f573091d17372c2ae0ae5e0f24f55b52"));
+ }
+}