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:

+ *
    + *
  1. 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.
  2. + *
  3. They allow synchronization to the head of the chain for new wallets/users much faster than syncing all + * headers from the genesis block.
  4. + *
  5. They mark each BIP30-violating block, which simplifies full verification logic quite significantly. BIP30 + * handles the case of blocks that contained duplicated coinbase transactions.
  6. + *
+ * + *

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")); + } +}