mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2025-03-10 00:09:31 +01:00
Add a CheckpointManager class and a BuildCheckpoints tool that saves a set of checkpoints to disk. By default there is one every difficulty transition period (two weeks), which means a relatively small amount of RAM needed to hold them all. There are only 111 checkpoints so far and old ones can be thinned out if needed.
This commit is contained in:
parent
b4c835c0cf
commit
0419887407
2 changed files with 211 additions and 0 deletions
|
@ -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.*;
|
||||
|
||||
/**
|
||||
* <p>Vends hard-coded {@link StoredBlock}s for blocks throughout the chain. Checkpoints serve several purposes:</p>
|
||||
* <ol>
|
||||
* <li>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.</li>
|
||||
* <li>They allow synchronization to the head of the chain for new wallets/users much faster than syncing all
|
||||
* headers from the genesis block.</li>
|
||||
* <li>They mark each BIP30-violating block, which simplifies full verification logic quite significantly. BIP30
|
||||
* handles the case of blocks that contained duplicated coinbase transactions.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
public class CheckpointManager {
|
||||
private static final Logger log = LoggerFactory.getLogger(CheckpointManager.class);
|
||||
|
||||
// Map of block header time to data.
|
||||
protected final TreeMap<Long, StoredBlock> checkpoints = new TreeMap<Long, StoredBlock>();
|
||||
|
||||
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<Long, StoredBlock> 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;
|
||||
}
|
||||
}
|
|
@ -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<Integer, StoredBlock> checkpoints = new TreeMap<Integer, StoredBlock>();
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue