diff --git a/core/src/main/java/com/google/bitcoin/core/FullPrunedBlockChain.java b/core/src/main/java/com/google/bitcoin/core/FullPrunedBlockChain.java index ffa480c4..cef2738f 100644 --- a/core/src/main/java/com/google/bitcoin/core/FullPrunedBlockChain.java +++ b/core/src/main/java/com/google/bitcoin/core/FullPrunedBlockChain.java @@ -84,10 +84,7 @@ public class FullPrunedBlockChain extends AbstractBlockChain { protected StoredBlock addToBlockStore(StoredBlock storedPrev, Block block) throws BlockStoreException, VerificationException { StoredBlock newBlock = storedPrev.build(block); - LinkedList transactions = new LinkedList(); - for (Transaction tx : block.transactions) - transactions.add(new StoredTransaction(tx, newBlock.getHeight())); - blockStore.put(newBlock, new StoredUndoableBlock(newBlock.getHeader().getHash(), transactions)); + blockStore.put(newBlock, new StoredUndoableBlock(newBlock.getHeader().getHash(), block.transactions)); return newBlock; } @@ -287,14 +284,14 @@ public class FullPrunedBlockChain extends AbstractBlockChain { } TransactionOutputChanges txOutChanges; try { - List transactions = block.getTransactions(); + List transactions = block.getTransactions(); if (transactions != null) { LinkedList txOutsSpent = new LinkedList(); LinkedList txOutsCreated = new LinkedList(); long sigOps = 0; final boolean enforcePayToScriptHash = newBlock.getHeader().getTimeSeconds() >= params.BIP16_ENFORCE_TIME; if (!params.isCheckpoint(newBlock.getHeight())) { - for(StoredTransaction tx : transactions) { + for(Transaction tx : transactions) { Sha256Hash hash = tx.getHash(); if (blockStore.hasUnspentOutputs(hash, tx.getOutputs().size())) throw new VerificationException("Block failed BIP30 test!"); @@ -306,7 +303,7 @@ public class FullPrunedBlockChain extends AbstractBlockChain { if (scriptVerificationExecutor.isShutdown()) scriptVerificationExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); List> listScriptVerificationResults = new ArrayList>(transactions.size()); - for(final StoredTransaction tx : transactions) { + for(final Transaction tx : transactions) { boolean isCoinBase = tx.isCoinBase(); BigInteger valueIn = BigInteger.ZERO; BigInteger valueOut = BigInteger.ZERO; @@ -340,7 +337,6 @@ public class FullPrunedBlockChain extends AbstractBlockChain { // 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 { scriptSig = in.getScriptSig(); @@ -356,7 +352,7 @@ public class FullPrunedBlockChain extends AbstractBlockChain { FutureTask future = new FutureTask(new Callable() { public VerificationException call() { try{ - scriptSig.correctlySpends(txCache, currentIndex, scriptPubKey, enforcePayToScriptHash); + scriptSig.correctlySpends(tx, currentIndex, scriptPubKey, enforcePayToScriptHash); } catch (ScriptException e) { return new VerificationException("Error verifying script: " + e.getMessage()); } catch (VerificationException e) { @@ -372,7 +368,7 @@ public class FullPrunedBlockChain extends AbstractBlockChain { } } Sha256Hash hash = tx.getHash(); - for (StoredTransactionOutput out : tx.getOutputs()) { + for (TransactionOutput out : tx.getOutputs()) { valueOut = valueOut.add(out.getValue()); StoredTransactionOutput newOut = new StoredTransactionOutput(hash, out.getIndex(), out.getValue(), newBlock.getHeight(), isCoinBase, diff --git a/core/src/main/java/com/google/bitcoin/core/StoredTransaction.java b/core/src/main/java/com/google/bitcoin/core/StoredTransaction.java deleted file mode 100644 index 08d78c20..00000000 --- a/core/src/main/java/com/google/bitcoin/core/StoredTransaction.java +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright 2012 Matt Corallo. - * - * 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 java.io.Serializable; -import java.util.LinkedList; -import java.util.List; - -//TODO: Move this to MemoryFullPrunedBlockStore and use a different method for on-disk storage (bitcoin serialization?) -/** - * A StoredTransaction message contains the information necessary to check a transaction later (ie after a reorg). - * It is used to avoid having to store the entire transaction when we only need its inputs+outputs. - * Its only really useful for MemoryFullPrunedBlockStore, and should probably be moved there - */ -public class StoredTransaction implements Serializable { - private static final long serialVersionUID = 6243881368122528323L; - - /** - * A transaction has some value and a script used for authenticating that the redeemer is allowed to spend - * this output. - */ - private List outputs; - private List inputs; - private long version; - private long lockTime; - private Sha256Hash hash; - - public StoredTransaction(Transaction tx, int height) { - inputs = new LinkedList(); - outputs = new LinkedList(); - for (TransactionInput in : tx.getInputs()) - inputs.add(new TransactionInput(in.params, null, in.getScriptBytes(), in.getOutpoint())); - for (TransactionOutput out : tx.getOutputs()) - outputs.add(new StoredTransactionOutput(null, out, height, tx.isCoinBase())); - this.version = tx.getVersion(); - this.lockTime = tx.getLockTime(); - this.hash = tx.getHash(); - } - - /** - * The lits of inputs in this transaction - */ - public List getInputs() { - return inputs; - } - - /** - * The lits of outputs in this transaction - * Note that the hashes of all of these are null - */ - public List getOutputs() { - return outputs; - } - - /** - * The hash of this stored transaction - */ - public Sha256Hash getHash() { - return hash; - } - - /** - * The lockTime of the stored transaction - */ - public long getLockTime() { - return lockTime; - } - - /** - * The version of the stored transaction - */ - public long getVersion() { - return version; - } - - /** - * A coinbase transaction is one that creates a new coin. They are the first transaction in each block and their - * value is determined by a formula that all implementations of BitCoin share. In 2011 the value of a coinbase - * transaction is 50 coins, but in future it will be less. A coinbase transaction is defined not only by its - * position in a block but by the data in the inputs. - */ - public boolean isCoinBase() { - return inputs.get(0).isCoinBase(); - } - - public String toString() { - return "Stored Transaction: " + hash.toString(); - } - - public int hashCode() { - return getHash().hashCode(); - } - - public boolean equals(Object o) { - if (!(o instanceof StoredTransaction)) return false; - return ((StoredTransaction) o).getHash().equals(this.getHash()); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/google/bitcoin/core/StoredTransactionOutput.java b/core/src/main/java/com/google/bitcoin/core/StoredTransactionOutput.java index 47f3c623..3951ef6e 100644 --- a/core/src/main/java/com/google/bitcoin/core/StoredTransactionOutput.java +++ b/core/src/main/java/com/google/bitcoin/core/StoredTransactionOutput.java @@ -16,6 +16,10 @@ package com.google.bitcoin.core; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.Serializable; import java.math.BigInteger; @@ -69,6 +73,35 @@ public class StoredTransactionOutput implements Serializable { this.scriptBytes = out.getScriptBytes(); } + public StoredTransactionOutput(InputStream in) throws IOException { + byte[] valueBytes = new byte[8]; + in.read(valueBytes, 0, 8); + value = BigInteger.valueOf(Utils.readInt64(valueBytes, 0)); + + int scriptBytesLength = ((in.read() & 0xFF) << 0) | + ((in.read() & 0xFF) << 8) | + ((in.read() & 0xFF) << 16) | + ((in.read() & 0xFF) << 24); + scriptBytes = new byte[scriptBytesLength]; + if (in.read(scriptBytes) != scriptBytesLength) + throw new EOFException(); + + byte[] hashBytes = new byte[32]; + if (in.read(hashBytes) != 32) + throw new EOFException(); + hash = new Sha256Hash(hashBytes); + + byte[] indexBytes = new byte[4]; + if (in.read(indexBytes) != 4) + throw new EOFException(); + index = Utils.readUint32(indexBytes, 0); + + height = ((in.read() & 0xFF) << 0) | + ((in.read() & 0xFF) << 8) | + ((in.read() & 0xFF) << 16) | + ((in.read() & 0xFF) << 24); + } + /** * The value which this Transaction output holds * @return the value @@ -121,4 +154,22 @@ public class StoredTransactionOutput implements Serializable { return ((StoredTransactionOutput) o).getIndex() == this.getIndex() && ((StoredTransactionOutput) o).getHash().equals(this.getHash()); } + + public void serializeToStream(OutputStream bos) throws IOException { + Utils.uint64ToByteStreamLE(value, bos); + + bos.write((int) (0xFF & (scriptBytes.length >> 0))); + bos.write((int) (0xFF & (scriptBytes.length >> 8))); + bos.write((int) (0xFF & (scriptBytes.length >> 16))); + bos.write((int) (0xFF & (scriptBytes.length >> 24))); + bos.write(scriptBytes); + + bos.write(hash.getBytes()); + Utils.uint32ToByteStreamLE(index, bos); + + bos.write((int) (0xFF & (height >> 0))); + bos.write((int) (0xFF & (height >> 8))); + bos.write((int) (0xFF & (height >> 16))); + bos.write((int) (0xFF & (height >> 24))); + } } \ No newline at end of file diff --git a/core/src/main/java/com/google/bitcoin/core/StoredUndoableBlock.java b/core/src/main/java/com/google/bitcoin/core/StoredUndoableBlock.java index 18fa9ca0..418824eb 100644 --- a/core/src/main/java/com/google/bitcoin/core/StoredUndoableBlock.java +++ b/core/src/main/java/com/google/bitcoin/core/StoredUndoableBlock.java @@ -34,7 +34,7 @@ public class StoredUndoableBlock implements Serializable { // Only one of either txOutChanges or transactions will be set private TransactionOutputChanges txOutChanges; - private List transactions; + private List transactions; public StoredUndoableBlock(Sha256Hash hash, TransactionOutputChanges txOutChanges) { this.blockHash = hash; @@ -42,7 +42,7 @@ public class StoredUndoableBlock implements Serializable { this.txOutChanges = txOutChanges; } - public StoredUndoableBlock(Sha256Hash hash, List transactions) { + public StoredUndoableBlock(Sha256Hash hash, List transactions) { this.blockHash = hash; this.txOutChanges = null; this.transactions = transactions; @@ -60,7 +60,7 @@ public class StoredUndoableBlock implements Serializable { * Get the full list of transactions if it is stored, otherwise null. * Only one of this and getTxOutChanges() will return a non-null value. */ - public List getTransactions() { + public List getTransactions() { return transactions; } 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 03cef950..9a475c33 100644 --- a/core/src/main/java/com/google/bitcoin/core/Transaction.java +++ b/core/src/main/java/com/google/bitcoin/core/Transaction.java @@ -139,22 +139,6 @@ public class Transaction extends ChildMessage implements Serializable { throws ProtocolException { super(params, msg, 0, parent, parseLazy, parseRetain, length); } - - /** - * Creates a transaction from the given StoredTransaction - * This is unsafe in that editing some aspects of this transaction may edit the stored transaction. - * Thus, this should only be used if the resulting transaction will only be used in a read-only manner. - */ - Transaction(NetworkParameters params, StoredTransaction tx) { - super(params); - this.version = tx.getVersion(); - this.lockTime = tx.getLockTime(); - this.inputs = new ArrayList(tx.getInputs()); - this.outputs = new ArrayList(tx.getOutputs().size()); - for (StoredTransactionOutput output : tx.getOutputs()) { - this.outputs.add(new TransactionOutput(params, this, output.getValue(), output.getScriptBytes())); - } - } /** * Returns the transaction hash as you see them in the block explorer. diff --git a/core/src/main/java/com/google/bitcoin/core/TransactionOutputChanges.java b/core/src/main/java/com/google/bitcoin/core/TransactionOutputChanges.java index df0cc570..abdf6c3f 100644 --- a/core/src/main/java/com/google/bitcoin/core/TransactionOutputChanges.java +++ b/core/src/main/java/com/google/bitcoin/core/TransactionOutputChanges.java @@ -16,7 +16,10 @@ package com.google.bitcoin.core; -import java.io.Serializable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.LinkedList; import java.util.List; /** @@ -25,9 +28,7 @@ import java.util.List; * It does contain outputs created that were spent later in the block, as those are needed for * BIP30 (no duplicate txid creation if the previous one was not fully spent prior to this block) verification. */ -public class TransactionOutputChanges implements Serializable { - private static final long serialVersionUID = -6169346729324181905L; - +public class TransactionOutputChanges { public final List txOutsCreated; public final List txOutsSpent; @@ -35,4 +36,42 @@ public class TransactionOutputChanges implements Serializable { this.txOutsCreated = txOutsCreated; this.txOutsSpent = txOutsSpent; } + + public TransactionOutputChanges(InputStream in) throws IOException { + int numOutsCreated = ((in.read() & 0xFF) << 0) | + ((in.read() & 0xFF) << 8) | + ((in.read() & 0xFF) << 16) | + ((in.read() & 0xFF) << 24); + txOutsCreated = new LinkedList(); + for (int i = 0; i < numOutsCreated; i++) + txOutsCreated.add(new StoredTransactionOutput(in)); + + int numOutsSpent = ((in.read() & 0xFF) << 0) | + ((in.read() & 0xFF) << 8) | + ((in.read() & 0xFF) << 16) | + ((in.read() & 0xFF) << 24); + txOutsSpent = new LinkedList(); + for (int i = 0; i < numOutsSpent; i++) + txOutsSpent.add(new StoredTransactionOutput(in)); + } + + public void serializeToStream(OutputStream bos) throws IOException { + int numOutsCreated = txOutsCreated.size(); + bos.write((int) (0xFF & (numOutsCreated >> 0))); + bos.write((int) (0xFF & (numOutsCreated >> 8))); + bos.write((int) (0xFF & (numOutsCreated >> 16))); + bos.write((int) (0xFF & (numOutsCreated >> 24))); + for (StoredTransactionOutput output : txOutsCreated) { + output.serializeToStream(bos); + } + + int numOutsSpent = txOutsSpent.size(); + bos.write((int) (0xFF & (numOutsSpent >> 0))); + bos.write((int) (0xFF & (numOutsSpent >> 8))); + bos.write((int) (0xFF & (numOutsSpent >> 16))); + bos.write((int) (0xFF & (numOutsSpent >> 24))); + for (StoredTransactionOutput output : txOutsSpent) { + output.serializeToStream(bos); + } + } } diff --git a/core/src/main/java/com/google/bitcoin/store/H2FullPrunedBlockStore.java b/core/src/main/java/com/google/bitcoin/store/H2FullPrunedBlockStore.java index 21396bfa..3e8dafc0 100644 --- a/core/src/main/java/com/google/bitcoin/store/H2FullPrunedBlockStore.java +++ b/core/src/main/java/com/google/bitcoin/store/H2FullPrunedBlockStore.java @@ -18,10 +18,13 @@ package com.google.bitcoin.store; import com.google.bitcoin.core.*; import com.google.common.collect.Lists; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.math.BigInteger; import java.sql.*; import java.util.LinkedList; @@ -199,7 +202,7 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore { StoredBlock storedGenesisHeader = new StoredBlock(params.genesisBlock.cloneAsHeader(), params.genesisBlock.getWork(), 0); // The coinbase in the genesis block is not spendable. This is because of how the reference client inits // its database - the genesis transaction isn't actually in the db so its spent flags can never be updated. - List genesisTransactions = Lists.newLinkedList(); + List genesisTransactions = Lists.newLinkedList(); StoredUndoableBlock storedGenesis = new StoredUndoableBlock(params.genesisBlock.getHash(), genesisTransactions); put(storedGenesisHeader, storedGenesis); setChainHead(storedGenesisHeader); @@ -339,15 +342,19 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore { byte[] txOutChanges = null; try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ObjectOutput out = new ObjectOutputStream(bos); if (undoableBlock.getTxOutChanges() != null) { - out.writeObject(undoableBlock.getTxOutChanges()); + undoableBlock.getTxOutChanges().serializeToStream(bos); txOutChanges = bos.toByteArray(); } else { - out.writeObject(undoableBlock.getTransactions()); + int numTxn = undoableBlock.getTransactions().size(); + bos.write((int) (0xFF & (numTxn >> 0))); + bos.write((int) (0xFF & (numTxn >> 8))); + bos.write((int) (0xFF & (numTxn >> 16))); + bos.write((int) (0xFF & (numTxn >> 24))); + for (Transaction tx : undoableBlock.getTransactions()) + tx.bitcoinSerialize(bos); transactions = bos.toByteArray(); } - out.close(); bos.close(); } catch (IOException e) { throw new BlockStoreException(e); @@ -436,7 +443,6 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore { } } - @SuppressWarnings("unchecked") public StoredUndoableBlock getUndoBlock(Sha256Hash hash) throws BlockStoreException { maybeConnect(); PreparedStatement s = null; @@ -456,14 +462,22 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore { byte[] transactions = results.getBytes(2); StoredUndoableBlock block; if (txOutChanges == null) { - Object transactionsObject = new ObjectInputStream(new ByteArrayInputStream(transactions)).readObject(); - if (!(((List) transactionsObject).get(0) instanceof StoredTransaction)) - throw new BlockStoreException("Corrupted StoredUndoableBlock"); - block = new StoredUndoableBlock(hash, (List) transactionsObject); + int offset = 0; + int numTxn = ((transactions[offset++] & 0xFF) << 0) | + ((transactions[offset++] & 0xFF) << 8) | + ((transactions[offset++] & 0xFF) << 16) | + ((transactions[offset++] & 0xFF) << 24); + List transactionList = new LinkedList(); + for (int i = 0; i < numTxn; i++) { + Transaction tx = new Transaction(params, transactions, offset); + transactionList.add(tx); + offset += tx.getMessageSize(); + } + block = new StoredUndoableBlock(hash, transactionList); } else { - ObjectInputStream obj = new ObjectInputStream(new ByteArrayInputStream(txOutChanges)); - Object transactionsObject = obj.readObject(); - block = new StoredUndoableBlock(hash, (TransactionOutputChanges) transactionsObject); + TransactionOutputChanges outChangesObject = + new TransactionOutputChanges(new ByteArrayInputStream(txOutChanges)); + block = new StoredUndoableBlock(hash, outChangesObject); } return block; } catch (SQLException ex) { @@ -474,7 +488,7 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore { } catch (ClassCastException e) { // Corrupted database. throw new BlockStoreException(e); - } catch (ClassNotFoundException e) { + } catch (ProtocolException e) { // Corrupted database. throw new BlockStoreException(e); } catch (IOException e) { diff --git a/core/src/main/java/com/google/bitcoin/store/MemoryFullPrunedBlockStore.java b/core/src/main/java/com/google/bitcoin/store/MemoryFullPrunedBlockStore.java index c2d1014b..d33fc237 100644 --- a/core/src/main/java/com/google/bitcoin/store/MemoryFullPrunedBlockStore.java +++ b/core/src/main/java/com/google/bitcoin/store/MemoryFullPrunedBlockStore.java @@ -25,6 +25,95 @@ import java.io.Serializable; import java.util.*; +/** + * A StoredTransaction message contains the information necessary to check a transaction later (ie after a reorg). + * It is used to avoid having to store the entire transaction when we only need its inputs+outputs. + */ +class StoredTransaction implements Serializable { + private static final long serialVersionUID = 6243881368122528323L; + + /** + * A transaction has some value and a script used for authenticating that the redeemer is allowed to spend + * this output. + */ + private List outputs; + private List inputs; + private long version; + private long lockTime; + private Sha256Hash hash; + + public StoredTransaction(NetworkParameters params, Transaction tx, int height) { + inputs = new LinkedList(); + outputs = new LinkedList(); + for (TransactionInput in : tx.getInputs()) + inputs.add(new TransactionInput(params, null, in.getScriptBytes(), in.getOutpoint())); + for (TransactionOutput out : tx.getOutputs()) + outputs.add(new StoredTransactionOutput(null, out, height, tx.isCoinBase())); + this.version = tx.getVersion(); + this.lockTime = tx.getLockTime(); + this.hash = tx.getHash(); + } + + /** + * The lits of inputs in this transaction + */ + public List getInputs() { + return inputs; + } + + /** + * The lits of outputs in this transaction + * Note that the hashes of all of these are null + */ + public List getOutputs() { + return outputs; + } + + /** + * The hash of this stored transaction + */ + public Sha256Hash getHash() { + return hash; + } + + /** + * The lockTime of the stored transaction + */ + public long getLockTime() { + return lockTime; + } + + /** + * The version of the stored transaction + */ + public long getVersion() { + return version; + } + + /** + * A coinbase transaction is one that creates a new coin. They are the first transaction in each block and their + * value is determined by a formula that all implementations of BitCoin share. In 2011 the value of a coinbase + * transaction is 50 coins, but in future it will be less. A coinbase transaction is defined not only by its + * position in a block but by the data in the inputs. + */ + public boolean isCoinBase() { + return inputs.get(0).isCoinBase(); + } + + public String toString() { + return "Stored Transaction: " + hash.toString(); + } + + public int hashCode() { + return getHash().hashCode(); + } + + public boolean equals(Object o) { + if (!(o instanceof StoredTransaction)) return false; + return ((StoredTransaction) o).getHash().equals(this.getHash()); + } +} + /** * Used as a key for memory map (to avoid having to think about NetworkParameters, * which is required for {@link TransactionOutPoint} @@ -240,7 +329,7 @@ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore { try { StoredBlock storedGenesisHeader = new StoredBlock(params.genesisBlock.cloneAsHeader(), params.genesisBlock.getWork(), 0); // The coinbase in the genesis block is not spendable - List genesisTransactions = Lists.newLinkedList(); + List genesisTransactions = Lists.newLinkedList(); StoredUndoableBlock storedGenesis = new StoredUndoableBlock(params.genesisBlock.getHash(), genesisTransactions); put(storedGenesisHeader, storedGenesis); setChainHead(storedGenesisHeader);