From 7ca87c078cf3892c522fabf3ea86d9edfcf45a16 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 26 Jul 2012 14:32:22 +0200 Subject: [PATCH] Add block timestamp and transaction finalization checks. This brings bitcoinj's block connection up to the reference client's AcceptBlock(). --- .../bitcoin/core/AbstractBlockChain.java | 36 ++++++++++++++++--- .../java/com/google/bitcoin/core/Block.java | 6 +++- .../com/google/bitcoin/core/Transaction.java | 18 ++++++++++ 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java b/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java index 39a07d3a..6600b188 100644 --- a/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java +++ b/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java @@ -272,7 +272,7 @@ public abstract class AbstractBlockChain { // Create a new StoredBlock from this block. It will throw away the transaction data so when block goes // out of scope we will reclaim the used memory. checkDifficultyTransitions(storedPrev, block); - connectBlock(block, storedPrev); + connectBlock(block, storedPrev, shouldVerifyTransactions()); } if (tryConnecting) @@ -282,14 +282,24 @@ public abstract class AbstractBlockChain { return true; } - private void connectBlock(Block block, StoredBlock storedPrev) + // expensiveChecks enables checks that require looking at blocks further back in the chain + // than the previous one when connecting (eg median timestamp check) + // It could be exposed, but for now we just set it to shouldVerifyTransactions() + private void connectBlock(Block block, StoredBlock storedPrev, boolean expensiveChecks) throws BlockStoreException, VerificationException, PrunedException { // Check that we aren't connecting a block that fails a checkpoint check if (!params.passesCheckpoint(storedPrev.getHeight() + 1, block.getHash())) throw new VerificationException("Block failed checkpoint lockin at " + (storedPrev.getHeight() + 1)); + if (shouldVerifyTransactions()) + for (Transaction tx : block.transactions) + if (!tx.isFinal(storedPrev.getHeight() + 1, block.getTimeSeconds())) + throw new VerificationException("Block contains non-final transaction"); StoredBlock head = getChainHead(); if (storedPrev.equals(head)) { + if (expensiveChecks && block.getTimeSeconds() <= getMedianTimestampOfRecentBlocks(head)) + throw new VerificationException("Block's timestamp is too early"); + // This block connects to the best known block, it is a normal continuation of the system. TransactionOutputChanges txOutChanges = null; if (shouldVerifyTransactions()) @@ -347,12 +357,26 @@ public abstract class AbstractBlockChain { sendTransactionsToWallet(newBlock, NewBlockType.SIDE_CHAIN, wallet, block.transactions); } } - + if (haveNewBestChain) - handleNewBestChain(storedPrev, newBlock, block); + handleNewBestChain(storedPrev, newBlock, block, expensiveChecks); } } + /** + * Gets the median timestamp of the last 11 blocks + */ + private long getMedianTimestampOfRecentBlocks(StoredBlock storedBlock) throws BlockStoreException { + long[] timestamps = new long[11]; + int unused = 9; + timestamps[10] = storedBlock.getHeader().getTimeSeconds(); + while (unused >= 0 && (storedBlock = storedBlock.getPrev(blockStore)) != null) + timestamps[unused--] = storedBlock.getHeader().getTimeSeconds(); + + Arrays.sort(timestamps, unused+1, 10); + return timestamps[unused + (11-unused)/2]; + } + /** * Disconnect each transaction in the block (after reading it from the block store) * Only called if(shouldVerifyTransactions()) @@ -367,7 +391,7 @@ public abstract class AbstractBlockChain { * if (shouldVerifyTransactions) * Either newChainHead needs to be in the block store as a FullStoredBlock, or (block != null && block.transactions != null) */ - private void handleNewBestChain(StoredBlock storedPrev, StoredBlock newChainHead, Block block) + private void handleNewBestChain(StoredBlock storedPrev, StoredBlock newChainHead, Block block, boolean expensiveChecks) throws BlockStoreException, VerificationException, PrunedException { // This chain has overtaken the one we currently believe is best. Reorganize is required. // @@ -400,6 +424,8 @@ public abstract class AbstractBlockChain { // Walk in ascending chronological order. for (Iterator it = newBlocks.descendingIterator(); it.hasNext();) { cursor = it.next(); + if (expensiveChecks && cursor.getHeader().getTimeSeconds() <= getMedianTimestampOfRecentBlocks(cursor.getPrev(blockStore))) + throw new VerificationException("Block's timestamp is too early during reorg"); TransactionOutputChanges txOutChanges; if (cursor != newChainHead || block == null) txOutChanges = connectTransactions(cursor); diff --git a/core/src/main/java/com/google/bitcoin/core/Block.java b/core/src/main/java/com/google/bitcoin/core/Block.java index 88bd651f..73b302e4 100644 --- a/core/src/main/java/com/google/bitcoin/core/Block.java +++ b/core/src/main/java/com/google/bitcoin/core/Block.java @@ -920,7 +920,11 @@ public class Block extends Message { } b.setPrevBlockHash(getHash()); - b.setTime(time); + // Don't let timestamp go backwards + if (getTimeSeconds() >= time) + b.setTime(getTimeSeconds() + 1); + else + b.setTime(time); b.solve(); try { b.verifyHeader(); diff --git a/core/src/main/java/com/google/bitcoin/core/Transaction.java b/core/src/main/java/com/google/bitcoin/core/Transaction.java index 876ef728..7f348e8c 100644 --- a/core/src/main/java/com/google/bitcoin/core/Transaction.java +++ b/core/src/main/java/com/google/bitcoin/core/Transaction.java @@ -44,6 +44,9 @@ import static com.google.bitcoin.core.Utils.*; public class Transaction extends ChildMessage implements Serializable { private static final Logger log = LoggerFactory.getLogger(Transaction.class); private static final long serialVersionUID = -8567546957352643140L; + + // Threshold for lockTime: below this value it is interpreted as block number, otherwise as timestamp. + static final int LOCKTIME_THRESHOLD = 500000000; // Tue Nov 5 00:53:20 1985 UTC // These are serialized in both bitcoin and java serialization. private long version; @@ -914,4 +917,19 @@ public class Transaction extends ChildMessage implements Serializable { throw new VerificationException("Coinbase input as input in non-coinbase transaction"); } } + + /** + * Returns true if this transaction is considered finalized and can be placed in a block + */ + public boolean isFinal(int height, long blockTimeSeconds) { + // Time based nLockTime implemented in 0.1.6 + if (lockTime == 0) + return true; + if (lockTime < (lockTime < LOCKTIME_THRESHOLD ? height : blockTimeSeconds)) + return true; + for (TransactionInput in : inputs) + if (in.hasSequence()) + return false; + return true; + } }