3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-01-31 07:12:17 +00:00

Make Script Verification multi-threaded in FullPrunedBlockChain.

This commit is contained in:
Matt Corallo 2012-08-06 03:47:38 +02:00 committed by Mike Hearn
parent c4d0364a60
commit 69f441b6c7

View File

@ -23,7 +23,15 @@ import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* <p>A FullPrunedBlockChain works in conjunction with a {@link FullPrunedBlockStore} to provide a fully verifying * <p>A FullPrunedBlockChain works in conjunction with a {@link FullPrunedBlockStore} to provide a fully verifying
* block chain. Fully verifying means all unspent transaction outputs are stored. Once a transaction output is spent * block chain. Fully verifying means all unspent transaction outputs are stored. Once a transaction output is spent
@ -32,6 +40,8 @@ import java.util.List;
* provides the same security guarantees as a regular Satoshi client does.</p> * provides the same security guarantees as a regular Satoshi client does.</p>
*/ */
public class FullPrunedBlockChain extends AbstractBlockChain { public class FullPrunedBlockChain extends AbstractBlockChain {
private static final Logger log = LoggerFactory.getLogger(FullPrunedBlockChain.class);
/** Keeps a map of block hashes to StoredBlocks. */ /** Keeps a map of block hashes to StoredBlocks. */
protected final FullPrunedBlockStore blockStore; protected final FullPrunedBlockStore blockStore;
@ -88,8 +98,10 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
//TODO: Remove lots of duplicated code in the two connectTransactions //TODO: Remove lots of duplicated code in the two connectTransactions
ExecutorService scriptVerificationExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
@Override @Override
protected TransactionOutputChanges connectTransactions(int height, Block block) protected synchronized TransactionOutputChanges connectTransactions(int height, Block block)
throws VerificationException, BlockStoreException { throws VerificationException, BlockStoreException {
if (block.transactions == null) if (block.transactions == null)
throw new RuntimeException("connectTransactions called with Block that didn't have transactions!"); throw new RuntimeException("connectTransactions called with Block that didn't have transactions!");
@ -102,6 +114,11 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
LinkedList<StoredTransactionOutput> txOutsCreated = new LinkedList<StoredTransactionOutput>(); LinkedList<StoredTransactionOutput> txOutsCreated = new LinkedList<StoredTransactionOutput>();
long sigOps = 0; long sigOps = 0;
boolean enforceBIP16 = block.getTimeSeconds() >= params.BIP16_ENFORCE_TIME; boolean enforceBIP16 = block.getTimeSeconds() >= params.BIP16_ENFORCE_TIME;
if (scriptVerificationExecutor.isShutdown())
scriptVerificationExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
List<Future<VerificationException>> listScriptVerificationResults = new ArrayList<Future<VerificationException>>(block.transactions.size());
try { try {
if (!params.isCheckpoint(height)) { if (!params.isCheckpoint(height)) {
// BIP30 violator blocks are ones that contain a duplicated transaction. They are all in the // BIP30 violator blocks are ones that contain a duplicated transaction. They are all in the
@ -154,11 +171,52 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
if (sigOps > Block.MAX_BLOCK_SIGOPS) if (sigOps > Block.MAX_BLOCK_SIGOPS)
throw new VerificationException("Too many P2SH SigOps in block"); throw new VerificationException("Too many P2SH SigOps in block");
} }
// All of these copies are terribly ugly, however without them,
// I see some odd concurrency issues where scripts throw exceptions
// (mostly "Attempted OP_* on empty stack" or similar) when they shouldn't.
// In my tests, total time spent in com.google.bitcoin.core when
// downloading the chain is < 0.5%, so doing this is no big efficiency issue.
// TODO: Find out the underlying issue and create a better work-around
final int currentIndex = index;
final Transaction txCache;
try { try {
txCache = new Transaction(params, tx.unsafeBitcoinSerialize());
} catch (ProtocolException e1) {
throw new RuntimeException(e1);
}
final Script scriptSig;
try {
scriptSig = in.getScriptSig();
} catch (ScriptException e2) {
throw new VerificationException("Error verifying script", e2);
}
final Script scriptPubKey;
try {
scriptPubKey = new Script(params, prevOut.getScriptBytes(), 0, prevOut.getScriptBytes().length);
} catch (ScriptException e1) {
throw new VerificationException("Error verifying script", e1);
}
FutureTask<VerificationException> future = new FutureTask<VerificationException>(new Callable<VerificationException>() {
public VerificationException call() {
try{
scriptSig.correctlySpends(txCache, currentIndex, scriptPubKey);
} catch (ScriptException e) {
return new VerificationException("Error verifying script: " + e.getMessage());
} catch (VerificationException e) {
return e;
}
return null;
}});
scriptVerificationExecutor.execute(future);
listScriptVerificationResults.add(future);
/*try{
in.getScriptSig().correctlySpends(tx, index, new Script(params, prevOut.getScriptBytes(), 0, prevOut.getScriptBytes().length)); in.getScriptSig().correctlySpends(tx, index, new Script(params, prevOut.getScriptBytes(), 0, prevOut.getScriptBytes().length));
} catch (ScriptException e) { } catch (ScriptException e) {
throw new VerificationException("Error verifying script: " + e.getMessage()); throw new VerificationException("Error verifying script: " + e.getMessage());
} }*/
blockStore.removeUnspentTransactionOutput(prevOut); blockStore.removeUnspentTransactionOutput(prevOut);
txOutsSpent.add(prevOut); txOutsSpent.add(prevOut);
} }
@ -186,10 +244,25 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
} }
if (totalFees.compareTo(params.MAX_MONEY) > 0 || block.getBlockInflation(height).add(totalFees).compareTo(coinbaseValue) < 0) if (totalFees.compareTo(params.MAX_MONEY) > 0 || block.getBlockInflation(height).add(totalFees).compareTo(coinbaseValue) < 0)
throw new VerificationException("Transaction fees out of range"); throw new VerificationException("Transaction fees out of range");
for (Future<VerificationException> future : listScriptVerificationResults) {
VerificationException e;
try {
e = future.get();
} catch (InterruptedException thrownE) {
throw new RuntimeException(thrownE); // Shouldn't happen
} catch (ExecutionException thrownE) {
log.error("Script.correctlySpends threw a non-normal exception: " + thrownE.getCause());
throw new VerificationException("Bug in Script.correctlySpends, likely script malformed in some new and interesting way.");
}
if (e != null)
throw e;
}
} catch (VerificationException e) { } catch (VerificationException e) {
scriptVerificationExecutor.shutdownNow();
blockStore.abortDatabaseBatchWrite(); blockStore.abortDatabaseBatchWrite();
throw e; throw e;
} catch (BlockStoreException e) { } catch (BlockStoreException e) {
scriptVerificationExecutor.shutdownNow();
blockStore.abortDatabaseBatchWrite(); blockStore.abortDatabaseBatchWrite();
throw e; throw e;
} }
@ -200,7 +273,7 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
/** /**
* Used during reorgs to connect a block previously on a fork * Used during reorgs to connect a block previously on a fork
*/ */
protected TransactionOutputChanges connectTransactions(StoredBlock newBlock) protected synchronized TransactionOutputChanges connectTransactions(StoredBlock newBlock)
throws VerificationException, BlockStoreException, PrunedException { throws VerificationException, BlockStoreException, PrunedException {
if (!params.passesCheckpoint(newBlock.getHeight(), newBlock.getHeader().getHash())) if (!params.passesCheckpoint(newBlock.getHeight(), newBlock.getHeader().getHash()))
throw new VerificationException("Block failed checkpoint lockin at " + newBlock.getHeight()); throw new VerificationException("Block failed checkpoint lockin at " + newBlock.getHeight());
@ -229,14 +302,18 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
} }
BigInteger totalFees = BigInteger.ZERO; BigInteger totalFees = BigInteger.ZERO;
BigInteger coinbaseValue = null; BigInteger coinbaseValue = null;
for(StoredTransaction tx : transactions) {
if (scriptVerificationExecutor.isShutdown())
scriptVerificationExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
List<Future<VerificationException>> listScriptVerificationResults = new ArrayList<Future<VerificationException>>(transactions.size());
for(final StoredTransaction tx : transactions) {
boolean isCoinBase = tx.isCoinBase(); boolean isCoinBase = tx.isCoinBase();
BigInteger valueIn = BigInteger.ZERO; BigInteger valueIn = BigInteger.ZERO;
BigInteger valueOut = BigInteger.ZERO; BigInteger valueOut = BigInteger.ZERO;
if (!isCoinBase) { if (!isCoinBase) {
for (int index = 0; index < tx.getInputs().size(); index++) { for (int index = 0; index < tx.getInputs().size(); index++) {
TransactionInput in = tx.getInputs().get(index); final TransactionInput in = tx.getInputs().get(index);
StoredTransactionOutput prevOut = blockStore.getTransactionOutput(in.getOutpoint().getHash(), final StoredTransactionOutput prevOut = blockStore.getTransactionOutput(in.getOutpoint().getHash(),
in.getOutpoint().getIndex()); in.getOutpoint().getIndex());
if (prevOut == null) if (prevOut == null)
throw new VerificationException("Attempted spend of a non-existent or already spent output!"); throw new VerificationException("Attempted spend of a non-existent or already spent output!");
@ -254,12 +331,42 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
if (sigOps > Block.MAX_BLOCK_SIGOPS) if (sigOps > Block.MAX_BLOCK_SIGOPS)
throw new VerificationException("Too many P2SH SigOps in block"); throw new VerificationException("Too many P2SH SigOps in block");
} }
// All of these copies are terribly ugly, however without them,
// I see some odd concurrency issues where scripts throw exceptions
// (mostly "Attempted OP_* on empty stack" or similar) when they shouldn't.
// In my tests, total time spent in com.google.bitcoin.core when
// downloading the chain is < 0.5%, so doing this is no big efficiency issue.
// TODO: Find out the underlying issue and create a better work-around
// TODO: Thoroughly test that this fixes the issue like the non-StoredBlock version does
final int currentIndex = index;
final Transaction txCache = new Transaction(params, tx);
final Script scriptSig;
try { try {
in.getScriptSig().correctlySpends(new Transaction(params, tx), index, scriptSig = in.getScriptSig();
new Script(params, prevOut.getScriptBytes(), 0, prevOut.getScriptBytes().length)); } catch (ScriptException e2) {
} catch (ScriptException e) { throw new VerificationException("Error verifying script", e2);
throw new VerificationException("Error verifying script", e);
} }
final Script scriptPubKey;
try {
scriptPubKey = new Script(params, prevOut.getScriptBytes(), 0, prevOut.getScriptBytes().length);
} catch (ScriptException e1) {
throw new VerificationException("Error verifying script", e1);
}
FutureTask<VerificationException> future = new FutureTask<VerificationException>(new Callable<VerificationException>() {
public VerificationException call() {
try{
scriptSig.correctlySpends(txCache, currentIndex, scriptPubKey);
} catch (ScriptException e) {
return new VerificationException("Error verifying script: " + e.getMessage());
} catch (VerificationException e) {
return e;
}
return null;
}});
scriptVerificationExecutor.execute(future);
listScriptVerificationResults.add(future);
blockStore.removeUnspentTransactionOutput(prevOut); blockStore.removeUnspentTransactionOutput(prevOut);
txOutsSpent.add(prevOut); txOutsSpent.add(prevOut);
} }
@ -289,8 +396,20 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
newBlock.getHeader().getBlockInflation(newBlock.getHeight()).add(totalFees).compareTo(coinbaseValue) < 0) newBlock.getHeader().getBlockInflation(newBlock.getHeight()).add(totalFees).compareTo(coinbaseValue) < 0)
throw new VerificationException("Transaction fees out of range"); throw new VerificationException("Transaction fees out of range");
txOutChanges = new TransactionOutputChanges(txOutsCreated, txOutsSpent); txOutChanges = new TransactionOutputChanges(txOutsCreated, txOutsSpent);
for (Future<VerificationException> future : listScriptVerificationResults) {
VerificationException e;
try {
e = future.get();
} catch (InterruptedException thrownE) {
throw new RuntimeException(thrownE); // Shouldn't happen
} catch (ExecutionException thrownE) {
log.error("Script.correctlySpends threw a non-normal exception: " + thrownE.getCause());
throw new VerificationException("Bug in Script.correctlySpends, likely script malformed in some new and interesting way.");
}
if (e != null)
throw e;
}
} else { } else {
// Use the undo data.
txOutChanges = block.getTxOutChanges(); txOutChanges = block.getTxOutChanges();
if (!params.isCheckpoint(newBlock.getHeight())) if (!params.isCheckpoint(newBlock.getHeight()))
for(StoredTransactionOutput out : txOutChanges.txOutsCreated) { for(StoredTransactionOutput out : txOutChanges.txOutsCreated) {
@ -304,9 +423,11 @@ public class FullPrunedBlockChain extends AbstractBlockChain {
blockStore.removeUnspentTransactionOutput(out); blockStore.removeUnspentTransactionOutput(out);
} }
} catch (VerificationException e) { } catch (VerificationException e) {
scriptVerificationExecutor.shutdownNow();
blockStore.abortDatabaseBatchWrite(); blockStore.abortDatabaseBatchWrite();
throw e; throw e;
} catch (BlockStoreException e) { } catch (BlockStoreException e) {
scriptVerificationExecutor.shutdownNow();
blockStore.abortDatabaseBatchWrite(); blockStore.abortDatabaseBatchWrite();
throw e; throw e;
} }