diff --git a/core/src/main/java/org/bitcoinj/core/FullPrunedBlockChain.java b/core/src/main/java/org/bitcoinj/core/FullPrunedBlockChain.java index 6107cf75..f272b68e 100644 --- a/core/src/main/java/org/bitcoinj/core/FullPrunedBlockChain.java +++ b/core/src/main/java/org/bitcoinj/core/FullPrunedBlockChain.java @@ -149,7 +149,51 @@ public class FullPrunedBlockChain extends AbstractBlockChain { return null; } } - + + /** + * Get the {@link Script} from the script bytes. + * @param scriptBytes The script bytes. + * @return The script. + */ + @Nullable + private Script getScript(byte[] scriptBytes) { + Script script = null; + try { + script = new Script(scriptBytes); + } catch (Exception e) { + log.warn("Unable to parse script"); + } + return script; + } + + /** + * Get the address from the {@link Script} if it exists otherwise return empty string "". + * @param script The script. + * @return The address. + */ + private String getScriptAddress(@Nullable Script script) { + String address = ""; + try { + if (script != null) { + address = script.getToAddress(params, true).toString(); + } + } catch (Exception e) { + } + return address; + } + + /** + * Get the {@link Script.ScriptType} of this script. + * @param script The script. + * @return The script type. + */ + private Script.ScriptType getScriptType(@Nullable Script script) { + if (script != null) { + return script.getScriptType(); + } + return Script.ScriptType.NO_TYPE; + } + @Override protected TransactionOutputChanges connectTransactions(int height, Block block) throws VerificationException, BlockStoreException { @@ -161,8 +205,8 @@ public class FullPrunedBlockChain extends AbstractBlockChain { blockStore.beginDatabaseBatchWrite(); - LinkedList txOutsSpent = new LinkedList(); - LinkedList txOutsCreated = new LinkedList(); + LinkedList txOutsSpent = new LinkedList(); + LinkedList txOutsCreated = new LinkedList(); long sigOps = 0; final Set verifyFlags = EnumSet.noneOf(VerifyFlag.class); if (block.getTimeSeconds() >= NetworkParameters.BIP16_ENFORCE_TIME) @@ -199,7 +243,7 @@ public class FullPrunedBlockChain extends AbstractBlockChain { // outputs. for (int index = 0; index < tx.getInputs().size(); index++) { TransactionInput in = tx.getInputs().get(index); - StoredTransactionOutput prevOut = blockStore.getTransactionOutput(in.getOutpoint().getHash(), + UTXO prevOut = blockStore.getTransactionOutput(in.getOutpoint().getHash(), in.getOutpoint().getIndex()); if (prevOut == null) throw new VerificationException("Attempted to spend a non-existent or already spent output!"); @@ -232,8 +276,14 @@ public class FullPrunedBlockChain extends AbstractBlockChain { for (TransactionOutput out : tx.getOutputs()) { valueOut = valueOut.add(out.getValue()); // For each output, add it to the set of unspent outputs so it can be consumed in future. - StoredTransactionOutput newOut = new StoredTransactionOutput(hash, out.getIndex(), out.getValue(), - height, isCoinBase, out.getScriptBytes()); + Script script = getScript(out.getScriptBytes()); + UTXO newOut = new UTXO(hash, + out.getIndex(), + out.getValue(), + height, isCoinBase, + out.getScriptBytes(), + getScriptAddress(script), + getScriptType(script).ordinal()); blockStore.addUnspentTransactionOutput(newOut); txOutsCreated.add(newOut); } @@ -304,8 +354,8 @@ public class FullPrunedBlockChain extends AbstractBlockChain { try { List transactions = block.getTransactions(); if (transactions != null) { - LinkedList txOutsSpent = new LinkedList(); - LinkedList txOutsCreated = new LinkedList(); + LinkedList txOutsSpent = new LinkedList(); + LinkedList txOutsCreated = new LinkedList(); long sigOps = 0; final Set verifyFlags = EnumSet.noneOf(VerifyFlag.class); if (newBlock.getHeader().getTimeSeconds() >= NetworkParameters.BIP16_ENFORCE_TIME) @@ -331,7 +381,7 @@ public class FullPrunedBlockChain extends AbstractBlockChain { if (!isCoinBase) { for (int index = 0; index < tx.getInputs().size(); index++) { final TransactionInput in = tx.getInputs().get(index); - final StoredTransactionOutput prevOut = blockStore.getTransactionOutput(in.getOutpoint().getHash(), + final UTXO prevOut = blockStore.getTransactionOutput(in.getOutpoint().getHash(), in.getOutpoint().getIndex()); if (prevOut == null) throw new VerificationException("Attempted spend of a non-existent or already spent output!"); @@ -355,9 +405,15 @@ public class FullPrunedBlockChain extends AbstractBlockChain { Sha256Hash hash = tx.getHash(); for (TransactionOutput out : tx.getOutputs()) { valueOut = valueOut.add(out.getValue()); - StoredTransactionOutput newOut = new StoredTransactionOutput(hash, out.getIndex(), out.getValue(), - newBlock.getHeight(), isCoinBase, - out.getScriptBytes()); + Script script = getScript(out.getScriptBytes()); + UTXO newOut = new UTXO(hash, + out.getIndex(), + out.getValue(), + newBlock.getHeight(), + isCoinBase, + out.getScriptBytes(), + getScriptAddress(script), + getScriptType(script).ordinal()); blockStore.addUnspentTransactionOutput(newOut); txOutsCreated.add(newOut); } @@ -400,14 +456,14 @@ public class FullPrunedBlockChain extends AbstractBlockChain { } else { txOutChanges = block.getTxOutChanges(); if (!params.isCheckpoint(newBlock.getHeight())) - for(StoredTransactionOutput out : txOutChanges.txOutsCreated) { + for(UTXO out : txOutChanges.txOutsCreated) { Sha256Hash hash = out.getHash(); if (blockStore.getTransactionOutput(hash, out.getIndex()) != null) throw new VerificationException("Block failed BIP30 test!"); } - for (StoredTransactionOutput out : txOutChanges.txOutsCreated) + for (UTXO out : txOutChanges.txOutsCreated) blockStore.addUnspentTransactionOutput(out); - for (StoredTransactionOutput out : txOutChanges.txOutsSpent) + for (UTXO out : txOutChanges.txOutsSpent) blockStore.removeUnspentTransactionOutput(out); } } catch (VerificationException e) { @@ -434,9 +490,9 @@ public class FullPrunedBlockChain extends AbstractBlockChain { StoredUndoableBlock undoBlock = blockStore.getUndoBlock(oldBlock.getHeader().getHash()); if (undoBlock == null) throw new PrunedException(oldBlock.getHeader().getHash()); TransactionOutputChanges txOutChanges = undoBlock.getTxOutChanges(); - for(StoredTransactionOutput out : txOutChanges.txOutsSpent) + for(UTXO out : txOutChanges.txOutsSpent) blockStore.addUnspentTransactionOutput(out); - for(StoredTransactionOutput out : txOutChanges.txOutsCreated) + for(UTXO out : txOutChanges.txOutsCreated) blockStore.removeUnspentTransactionOutput(out); } catch (PrunedException e) { blockStore.abortDatabaseBatchWrite(); diff --git a/core/src/main/java/org/bitcoinj/core/TransactionInput.java b/core/src/main/java/org/bitcoinj/core/TransactionInput.java index ac41d0b8..92930bd6 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionInput.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionInput.java @@ -91,7 +91,11 @@ public class TransactionInput extends ChildMessage implements Serializable { TransactionInput(NetworkParameters params, Transaction parentTransaction, TransactionOutput output) { super(params); long outputIndex = output.getIndex(); - outpoint = new TransactionOutPoint(params, outputIndex, output.getParentTransaction()); + if(output.getParentTransaction() != null ) { + outpoint = new TransactionOutPoint(params, outputIndex, output.getParentTransaction()); + } else { + outpoint = new TransactionOutPoint(params, output); + } scriptBytes = EMPTY_ARRAY; sequence = NO_SEQUENCE; setParent(parentTransaction); diff --git a/core/src/main/java/org/bitcoinj/core/TransactionOutPoint.java b/core/src/main/java/org/bitcoinj/core/TransactionOutPoint.java index 12677210..b9d39791 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionOutPoint.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionOutPoint.java @@ -46,6 +46,9 @@ public class TransactionOutPoint extends ChildMessage implements Serializable { // It points to the connected transaction. Transaction fromTx; + // The connected output. + private TransactionOutput connectedOutput; + public TransactionOutPoint(NetworkParameters params, long index, @Nullable Transaction fromTx) { super(params); this.index = index; @@ -66,6 +69,11 @@ public class TransactionOutPoint extends ChildMessage implements Serializable { length = MESSAGE_LENGTH; } + public TransactionOutPoint(NetworkParameters params, TransactionOutput connectedOutput) { + this(params, connectedOutput.getIndex(), connectedOutput.getParentTransactionHash()); + this.connectedOutput = connectedOutput; + } + /** /** * Deserializes the message. This is usually part of a transaction message. @@ -120,8 +128,12 @@ public class TransactionOutPoint extends ChildMessage implements Serializable { */ @Nullable public TransactionOutput getConnectedOutput() { - if (fromTx == null) return null; - return fromTx.getOutputs().get((int) index); + if (fromTx != null) { + return fromTx.getOutputs().get((int) index); + } else if (connectedOutput != null) { + return connectedOutput; + } + return null; } /** diff --git a/core/src/main/java/org/bitcoinj/core/TransactionOutput.java b/core/src/main/java/org/bitcoinj/core/TransactionOutput.java index 854a4461..27f8d6f7 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionOutput.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionOutput.java @@ -91,7 +91,7 @@ public class TransactionOutput extends ChildMessage implements Serializable { /** * Creates an output that sends 'value' to the given address (public key hash). The amount should be created with - * something like {@link Utils#valueOf(int, int)}. Typically you would use + * something like {@link Coin#valueOf(int, int)}. Typically you would use * {@link Transaction#addOutput(Coin, Address)} instead of creating a TransactionOutput directly. */ public TransactionOutput(NetworkParameters params, @Nullable Transaction parent, Coin value, Address to) { @@ -100,7 +100,7 @@ public class TransactionOutput extends ChildMessage implements Serializable { /** * Creates an output that sends 'value' to the given public key using a simple CHECKSIG script (no addresses). The - * amount should be created with something like {@link Utils#valueOf(int, int)}. Typically you would use + * amount should be created with something like {@link Coin#valueOf(int, int)}. Typically you would use * {@link Transaction#addOutput(Coin, ECKey)} instead of creating an output directly. */ public TransactionOutput(NetworkParameters params, @Nullable Transaction parent, Coin value, ECKey to) { @@ -384,10 +384,42 @@ public class TransactionOutput extends ChildMessage implements Serializable { } /** - * Returns the transaction that owns this output, or throws NullPointerException if unowned. + * Returns the transaction that owns this output. */ + @Nullable public Transaction getParentTransaction() { - return checkNotNull((Transaction) parent, "Free-standing TransactionOutput"); + if(parent != null) { + return (Transaction) parent; + } + return null; + } + + /** + * Returns the transaction hash that owns this output. + */ + @Nullable + public Sha256Hash getParentTransactionHash() { + if (getParentTransaction() != null) { + return getParentTransaction().getHash(); + } + return null; + } + + /** + * Returns the depth in blocks of the parent tx. + * + *

If the transaction appears in the top block, the depth is one. If it's anything else (pending, dead, unknown) + * then -1.

+ * @return The tx depth or -1. + */ + public int getParentTransactionDepthInBlocks() { + if (getParentTransaction() != null) { + TransactionConfidence confidence = getParentTransaction().getConfidence(); + if (confidence.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) { + return confidence.getDepthInBlocks(); + } + } + return -1; } /** diff --git a/core/src/main/java/org/bitcoinj/core/TransactionOutputChanges.java b/core/src/main/java/org/bitcoinj/core/TransactionOutputChanges.java index 30ad7779..5de25848 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionOutputChanges.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionOutputChanges.java @@ -29,10 +29,10 @@ import java.util.List; * BIP30 (no duplicate txid creation if the previous one was not fully spent prior to this block) verification.

*/ public class TransactionOutputChanges { - public final List txOutsCreated; - public final List txOutsSpent; + public final List txOutsCreated; + public final List txOutsSpent; - public TransactionOutputChanges(List txOutsCreated, List txOutsSpent) { + public TransactionOutputChanges(List txOutsCreated, List txOutsSpent) { this.txOutsCreated = txOutsCreated; this.txOutsSpent = txOutsSpent; } @@ -42,17 +42,17 @@ public class TransactionOutputChanges { ((in.read() & 0xFF) << 8) | ((in.read() & 0xFF) << 16) | ((in.read() & 0xFF) << 24); - txOutsCreated = new LinkedList(); + txOutsCreated = new LinkedList(); for (int i = 0; i < numOutsCreated; i++) - txOutsCreated.add(new StoredTransactionOutput(in)); + txOutsCreated.add(new UTXO(in)); int numOutsSpent = ((in.read() & 0xFF) << 0) | ((in.read() & 0xFF) << 8) | ((in.read() & 0xFF) << 16) | ((in.read() & 0xFF) << 24); - txOutsSpent = new LinkedList(); + txOutsSpent = new LinkedList(); for (int i = 0; i < numOutsSpent; i++) - txOutsSpent.add(new StoredTransactionOutput(in)); + txOutsSpent.add(new UTXO(in)); } public void serializeToStream(OutputStream bos) throws IOException { @@ -61,7 +61,7 @@ public class TransactionOutputChanges { bos.write(0xFF & (numOutsCreated >> 8)); bos.write(0xFF & (numOutsCreated >> 16)); bos.write(0xFF & (numOutsCreated >> 24)); - for (StoredTransactionOutput output : txOutsCreated) { + for (UTXO output : txOutsCreated) { output.serializeToStream(bos); } @@ -70,7 +70,7 @@ public class TransactionOutputChanges { bos.write(0xFF & (numOutsSpent >> 8)); bos.write(0xFF & (numOutsSpent >> 16)); bos.write(0xFF & (numOutsSpent >> 24)); - for (StoredTransactionOutput output : txOutsSpent) { + for (UTXO output : txOutsSpent) { output.serializeToStream(bos); } } diff --git a/core/src/main/java/org/bitcoinj/core/StoredTransactionOutput.java b/core/src/main/java/org/bitcoinj/core/UTXO.java similarity index 69% rename from core/src/main/java/org/bitcoinj/core/StoredTransactionOutput.java rename to core/src/main/java/org/bitcoinj/core/UTXO.java index 2a440cc0..f8332972 100644 --- a/core/src/main/java/org/bitcoinj/core/StoredTransactionOutput.java +++ b/core/src/main/java/org/bitcoinj/core/UTXO.java @@ -16,15 +16,19 @@ package org.bitcoinj.core; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.*; import java.math.BigInteger; /** - * A StoredTransactionOutput message contains the information necessary to check a spending transaction. + * A UTXO message contains the information necessary to check a spending transaction. * It avoids having to store the entire parentTransaction just to get the hash and index. - * Its only really useful for MemoryFullPrunedBlockStore, and should probably be moved there + * Useful when working with free standing outputs. */ -public class StoredTransactionOutput implements Serializable { +public class UTXO implements Serializable { + private static final Logger log = LoggerFactory.getLogger(UTXO.class); private static final long serialVersionUID = -8744924157056340509L; /** @@ -42,62 +46,88 @@ public class StoredTransactionOutput implements Serializable { private int height; /** If this output is from a coinbase tx */ private boolean coinbase; + /** The address of this output */ + private String address; + /** The type of this address */ + private int addressType; /** * Creates a stored transaction output. - * @param hash The hash of the containing transaction + * @param hash The hash of the containing transaction. * @param index The outpoint. * @param value The value available. * @param height The height this output was created in. * @param coinbase The coinbase flag. * @param scriptBytes The script bytes. */ - public StoredTransactionOutput(Sha256Hash hash, long index, Coin value, int height, boolean coinbase, byte[] scriptBytes) { + public UTXO(Sha256Hash hash, + long index, + Coin value, + int height, + boolean coinbase, + byte[] scriptBytes) { this.hash = hash; this.index = index; this.value = value; this.height = height; this.scriptBytes = scriptBytes; this.coinbase = coinbase; + this.address = ""; + this.addressType = 0; } - public StoredTransactionOutput(Sha256Hash hash, TransactionOutput out, int height, boolean coinbase) { - this.hash = hash; - this.index = out.getIndex(); - this.value = out.getValue(); - this.height = height; - this.scriptBytes = out.getScriptBytes(); - this.coinbase = coinbase; + /** + * Creates a stored transaction output. + * @param hash The hash of the containing transaction. + * @param index The outpoint. + * @param value The value available. + * @param height The height this output was created in. + * @param coinbase The coinbase flag. + * @param scriptBytes The script bytes. + * @param address The address. + * @param addressType The address type. + */ + public UTXO(Sha256Hash hash, + long index, + Coin value, + int height, + boolean coinbase, + byte[] scriptBytes, + String address, + int addressType) { + this(hash, index, value, height, coinbase, scriptBytes); + this.address = address; + this.addressType = addressType; } - public StoredTransactionOutput(InputStream in) throws IOException { + public UTXO(InputStream in) throws IOException { byte[] valueBytes = new byte[8]; if (in.read(valueBytes, 0, 8) != 8) throw new EOFException(); value = Coin.valueOf(Utils.readInt64(valueBytes, 0)); - + int scriptBytesLength = ((in.read() & 0xFF) << 0) | - ((in.read() & 0xFF) << 8) | - ((in.read() & 0xFF) << 16) | - ((in.read() & 0xFF) << 24); + ((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); + ((in.read() & 0xFF) << 8) | + ((in.read() & 0xFF) << 16) | + ((in.read() & 0xFF) << 24); byte[] coinbaseByte = new byte[1]; in.read(coinbaseByte); @@ -156,6 +186,22 @@ public class StoredTransactionOutput implements Serializable { return coinbase; } + /** + * The address of this output. + * @return The address. + */ + public String getAddress() { + return address; + } + + /** + * The type of the address. + * @return The address type. + */ + public int getAddressType() { + return addressType; + } + @Override public String toString() { return String.format("Stored TxOut of %s (%s:%d)", value.toFriendlyString(), hash.toString(), index); @@ -170,23 +216,23 @@ public class StoredTransactionOutput implements Serializable { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - StoredTransactionOutput other = (StoredTransactionOutput) o; + UTXO other = (UTXO) o; return getHash().equals(other.getHash()) && - getIndex() == other.getIndex(); + getIndex() == other.getIndex(); } public void serializeToStream(OutputStream bos) throws IOException { Utils.uint64ToByteStreamLE(BigInteger.valueOf(value.value), bos); - + bos.write(0xFF & scriptBytes.length >> 0); bos.write(0xFF & scriptBytes.length >> 8); bos.write(0xFF & (scriptBytes.length >> 16)); bos.write(0xFF & (scriptBytes.length >> 24)); bos.write(scriptBytes); - + bos.write(hash.getBytes()); Utils.uint32ToByteStreamLE(index, bos); - + bos.write(0xFF & (height >> 0)); bos.write(0xFF & (height >> 8)); bos.write(0xFF & (height >> 16)); @@ -200,4 +246,4 @@ public class StoredTransactionOutput implements Serializable { } bos.write(coinbaseByte); } -} +} \ No newline at end of file diff --git a/core/src/main/java/org/bitcoinj/core/UTXOProvider.java b/core/src/main/java/org/bitcoinj/core/UTXOProvider.java new file mode 100644 index 00000000..abec341b --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/UTXOProvider.java @@ -0,0 +1,49 @@ +/** + * Copyright 2014 Kalpesh Parmar. + * + * 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 org.bitcoinj.core; + +import java.util.List; + +/** + * A UTXOProvider encapsulates functionality for returning unspent transaction outputs, + * for use by the wallet or other code that crafts spends. + * + *

A {@link org.bitcoinj.store.FullPrunedBlockStore} is an internal implementation within bitcoinj.

+ */ +public interface UTXOProvider { + + /** + * // TODO currently the access to outputs is by address. Change to ECKey + * Get the list of {@link UTXO}'s for a given address. + * @param addresses List of address. + * @return The list of transaction outputs. + * @throws UTXOProvider If there is an error. + */ + List getOpenTransactionOutputs(List
addresses) throws UTXOProviderException; + + /** + * Get the height of the chain head. + * @return The chain head height. + * @throws UTXOProvider If there is an error. + */ + int getChainHeadHeight() throws UTXOProviderException; + + /** + * The {@link NetworkParameters} of this provider. + * @return The network parameters. + */ + NetworkParameters getParams(); +} diff --git a/core/src/main/java/org/bitcoinj/core/UTXOProviderException.java b/core/src/main/java/org/bitcoinj/core/UTXOProviderException.java new file mode 100644 index 00000000..71f97fbb --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/UTXOProviderException.java @@ -0,0 +1,34 @@ +/** + * Copyright 2014 Kalpesh Parmar. + * + * 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 org.bitcoinj.core; + +public class UTXOProviderException extends Exception { + public UTXOProviderException() { + super(); + } + + public UTXOProviderException(String message) { + super(message); + } + + public UTXOProviderException(String message, Throwable cause) { + super(message, cause); + } + + public UTXOProviderException(Throwable cause) { + super(cause); + } +} diff --git a/core/src/main/java/org/bitcoinj/core/Wallet.java b/core/src/main/java/org/bitcoinj/core/Wallet.java index 6c34019c..76b5e7bf 100644 --- a/core/src/main/java/org/bitcoinj/core/Wallet.java +++ b/core/src/main/java/org/bitcoinj/core/Wallet.java @@ -38,6 +38,7 @@ import org.bitcoinj.script.ScriptChunk; import org.bitcoinj.signers.LocalTransactionSigner; import org.bitcoinj.signers.MissingSigResolutionSigner; import org.bitcoinj.signers.TransactionSigner; +import org.bitcoinj.store.FullPrunedBlockStore; import org.bitcoinj.store.UnreadableWalletException; import org.bitcoinj.store.WalletProtobufSerializer; import org.bitcoinj.utils.BaseTaggableObject; @@ -58,7 +59,6 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; import static com.google.common.base.Preconditions.*; @@ -209,6 +209,9 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha // Objects that perform transaction signing. Applied subsequently one after another @GuardedBy("lock") private List signers; + // If this is set then the wallet selects spendable candidate outputs from a UTXO provider. + @Nullable volatile private UTXOProvider vUTXOProvider; + /** * Creates a new, empty wallet with no keys and no transactions. If you want to restore a wallet from disk instead, * see loadFromFile. @@ -3600,15 +3603,20 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha public LinkedList calculateAllSpendCandidates(boolean excludeImmatureCoinbases) { lock.lock(); try { - LinkedList candidates = Lists.newLinkedList(); - for (Transaction tx : Iterables.concat(unspent.values(), pending.values())) { - // Do not try and spend coinbases that were mined too recently, the protocol forbids it. - if (excludeImmatureCoinbases && !tx.isMature()) continue; - for (TransactionOutput output : tx.getOutputs()) { - if (!output.isAvailableForSpending()) continue; - if (!output.isMine(this)) continue; - candidates.add(output); + LinkedList candidates; + if (vUTXOProvider == null) { + candidates = Lists.newLinkedList(); + for (Transaction tx : Iterables.concat(unspent.values(), pending.values())) { + // Do not try and spend coinbases that were mined too recently, the protocol forbids it. + if (excludeImmatureCoinbases && !tx.isMature()) continue; + for (TransactionOutput output : tx.getOutputs()) { + if (!output.isAvailableForSpending()) continue; + if (!output.isMine(this)) continue; + candidates.add(output); + } } + } else { + candidates = calculateAllSpendCandidatesFromUTXOProvider(excludeImmatureCoinbases); } return candidates; } finally { @@ -3616,6 +3624,63 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha } } + /** + * Returns the spendable candidates from the {@link UTXOProvider} based on keys that the wallet contains. + * @return The list of candidates. + */ + protected LinkedList calculateAllSpendCandidatesFromUTXOProvider(boolean excludeImmatureCoinbases){ + checkState(lock.isHeldByCurrentThread()); + LinkedList candidates = Lists.newLinkedList(); + try { + int chainHeight = vUTXOProvider.getChainHeadHeight(); + for (UTXO output : getStoredOutputsFromUTXOProvider()) { + boolean coinbase = output.isCoinbase(); + int depth = chainHeight - output.getHeight() + 1; // the current depth of the output (1 = same as head). + // Do not try and spend coinbases that were mined too recently, the protocol forbids it. + if (!excludeImmatureCoinbases || !coinbase || depth >= params.getSpendableCoinbaseDepth()) { + candidates.add(new FreeStandingTransactionOutput(params, output, chainHeight)); + } + } + } catch (UTXOProviderException e) { + throw new RuntimeException("UTXO provider error", e); + } + // We need to handle the pending transactions that we know about. + for (Transaction tx : Iterables.concat(pending.values())) { + // Remove the spent outputs. + for(TransactionInput input : tx.getInputs()) { + if (input.getConnectedOutput().isMine(this)) { + candidates.remove(input.getConnectedOutput()); + } + } + // Add change outputs. Do not try and spend coinbases that were mined too recently, the protocol forbids it. + if (!excludeImmatureCoinbases || tx.isMature()) { + for (TransactionOutput output : tx.getOutputs()) { + if (output.isAvailableForSpending() && output.isMine(this)) { + candidates.add(output); + } + } + } + } + return candidates; + } + + /** + * Get all the {@link UTXO}'s from the {@link UTXOProvider} based on keys that the + * wallet contains. + * @return The list of stored outputs. + */ + protected List getStoredOutputsFromUTXOProvider() throws UTXOProviderException { + List candidates = new ArrayList(); + List keys = getActiveKeychain().getLeafKeys(); + List
addresses = new ArrayList
(); + for (ECKey key : keys) { + Address address = new Address(params, key.getPubKeyHash()); + addresses.add(address); + } + candidates.addAll(vUTXOProvider.getOpenTransactionOutputs(addresses)); + return candidates; + } + /** Returns the {@link CoinSelector} object which controls which outputs can be spent by this wallet. */ public CoinSelector getCoinSelector() { lock.lock(); @@ -3650,8 +3715,97 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha setCoinSelector(AllowUnconfirmedCoinSelector.get()); } + /** + * Get the {@link UTXOProvider}. + * @return The UTXO provider. + */ + @Nullable public UTXOProvider getUTXOProvider() { + lock.lock(); + try { + return vUTXOProvider; + } finally { + lock.unlock(); + } + } + + /** + * Set the {@link UTXOProvider}. + * + *

The wallet will query the provider for the spendable candidates. + * The spendable candidates are the outputs controlled exclusively + * by private keys contained in the wallet.

+ * + *

Note that the associated provider must be reattached after a wallet is loaded from disk. + * The association is not serialized.

+ * + * @param vUTXOProvider The UTXO provider. + */ + public void setUTXOProvider(@Nullable FullPrunedBlockStore vUTXOProvider) { + lock.lock(); + try { + checkArgument(vUTXOProvider == null ? true : vUTXOProvider.getParams().equals(params)); + this.vUTXOProvider = vUTXOProvider; + } finally { + lock.unlock(); + } + } + //endregion + /******************************************************************************************************************/ + + /** + * A custom {@link TransactionOutput} that is free standing. This contains all the information + * required for spending without actually having all the linked data (i.e parent tx). + * + */ + private class FreeStandingTransactionOutput extends TransactionOutput { + private UTXO output; + private int chainHeight; + + /** + * Construct a free standing Transaction Output. + * @param params The network parameters. + * @param output The stored output (free standing). + */ + public FreeStandingTransactionOutput(NetworkParameters params, UTXO output, int chainHeight) { + super(params, null, output.getValue(), output.getScriptBytes()); + this.output = output; + this.chainHeight = chainHeight; + } + + /** + * Get the {@link UTXO}. + * @return The stored output. + */ + public UTXO getUTXO() { + return output; + } + + /** + * Get the depth withing the chain of the parent tx, depth is 1 if it the output height is the height of + * the latest block. + * @return The depth. + */ + @Override + public int getParentTransactionDepthInBlocks() { + return chainHeight - output.getHeight() + 1; + } + + @Override + public int getIndex() { + return (int) output.getIndex(); + } + + @Override + public Sha256Hash getParentTransactionHash() { + return output.getHash(); + } + } + + /******************************************************************************************************************/ + + /******************************************************************************************************************/ private static class TxOffsetPair implements Comparable { diff --git a/core/src/main/java/org/bitcoinj/script/Script.java b/core/src/main/java/org/bitcoinj/script/Script.java index 5d05dd44..d5c396f3 100644 --- a/core/src/main/java/org/bitcoinj/script/Script.java +++ b/core/src/main/java/org/bitcoinj/script/Script.java @@ -51,6 +51,15 @@ import static com.google.common.base.Preconditions.*; */ public class Script { + /** Enumeration to encapsulate the type of this script. */ + public enum ScriptType { + // Do NOT change the ordering of the following definitions because their ordinals are stored in databases. + NO_TYPE, + P2PKH, + PUB_KEY, + P2SH + }; + /** Flags to pass to {@link Script#correctlySpends(Transaction, long, Script, Set)}. */ public enum VerifyFlag { P2SH, // Enable BIP16-style subscript evaluation. @@ -1458,6 +1467,22 @@ public class Script { return getProgram(); } + /** + * Get the {@link org.bitcoinj.script.Script.ScriptType}. + * @return The script type. + */ + public ScriptType getScriptType() { + ScriptType type = ScriptType.NO_TYPE; + if (isSentToAddress()) { + type = ScriptType.P2PKH; + } else if (isSentToRawPubKey()) { + type = ScriptType.PUB_KEY; + } else if (isPayToScriptHash()) { + type = ScriptType.P2SH; + } + return type; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/core/src/main/java/org/bitcoinj/store/BlockStore.java b/core/src/main/java/org/bitcoinj/store/BlockStore.java index bae6090f..f1fc5147 100644 --- a/core/src/main/java/org/bitcoinj/store/BlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/BlockStore.java @@ -16,6 +16,7 @@ package org.bitcoinj.store; +import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.StoredBlock; @@ -58,4 +59,10 @@ public interface BlockStore { /** Closes the store. */ void close() throws BlockStoreException; + + /** + * Get the {@link org.bitcoinj.core.NetworkParameters} of this store. + * @return The network params. + */ + NetworkParameters getParams(); } diff --git a/core/src/main/java/org/bitcoinj/store/DatabaseFullPrunedBlockStore.java b/core/src/main/java/org/bitcoinj/store/DatabaseFullPrunedBlockStore.java index 621f15eb..4a3aed8f 100644 --- a/core/src/main/java/org/bitcoinj/store/DatabaseFullPrunedBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/DatabaseFullPrunedBlockStore.java @@ -19,7 +19,6 @@ package org.bitcoinj.store; import com.google.common.collect.Lists; import org.bitcoinj.core.*; -import org.bitcoinj.script.Script; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -112,7 +111,7 @@ public abstract class DatabaseFullPrunedBlockStore implements FullPrunedBlockSto private static final String UPDATE_UNDOABLEBLOCKS_SQL = "UPDATE undoableBlocks SET txOutChanges=?, transactions=? WHERE hash = ?"; private static final String DELETE_UNDOABLEBLOCKS_SQL = "DELETE FROM undoableBlocks WHERE height <= ?"; - private static final String SELECT_OPENOUTPUTS_SQL = "SELECT height, value, scriptBytes, coinbase FROM openOutputs WHERE hash = ? AND index = ?"; + private static final String SELECT_OPENOUTPUTS_SQL = "SELECT height, value, scriptBytes, coinbase, toaddress, addresstargetable FROM openOutputs WHERE hash = ? AND index = ?"; private static final String SELECT_OPENOUTPUTS_COUNT_SQL = "SELECT COUNT(*) FROM openOutputs WHERE hash = ?"; private static final String INSERT_OPENOUTPUTS_SQL = "INSERT INTO openOutputs (hash, index, height, value, scriptBytes, toAddress, addressTargetable, coinbase) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; private static final String DELETE_OPENOUTPUTS_SQL = "DELETE FROM openOutputs WHERE hash = ? AND index = ?"; @@ -123,8 +122,7 @@ public abstract class DatabaseFullPrunedBlockStore implements FullPrunedBlockSto private static final String SELECT_DUMP_UNDOABLEBLOCKS_SQL = "SELECT txOutChanges, transactions FROM undoableBlocks"; private static final String SELECT_DUMP_OPENOUTPUTS_SQL = "SELECT value, scriptBytes FROM openOutputs"; - private static final String SELECT_TRANSACTION_OUTPUTS_SQL = "SELECT value, scriptBytes, height FROM openOutputs where toaddress = ?"; - private static final String SELECT_TRANSACTION_OUTPUTS_WITH_HEIGHT_SQL = "SELECT value, scriptBytes, height FROM openOutputs where toaddress = ? AND height <= ?"; + private static final String SELECT_TRANSACTION_OUTPUTS_SQL = "SELECT hash, value, scriptBytes, height, index, coinbase, toaddress, addresstargetable FROM openOutputs where toaddress = ?"; // Select the balance of an address SQL. private static final String SELECT_BALANCE_SQL = "select sum(value) from openoutputs where toaddress = ?"; @@ -259,14 +257,6 @@ public abstract class DatabaseFullPrunedBlockStore implements FullPrunedBlockSto return SELECT_TRANSACTION_OUTPUTS_SQL; } - /** - * Get the SQL to select the transaction outputs for a given address and height seen. - * @return The SQL prepared statement. - */ - protected String getTrasactionOutputWithHeightSelectSQL() { - return SELECT_TRANSACTION_OUTPUTS_WITH_HEIGHT_SQL; - } - /** * Get the SQL to drop all the tables (DDL). * @return The SQL drop statements. @@ -921,7 +911,7 @@ public abstract class DatabaseFullPrunedBlockStore implements FullPrunedBlockSto } @Override - public StoredTransactionOutput getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException { + public UTXO getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException { maybeConnect(); PreparedStatement s = null; try { @@ -939,7 +929,16 @@ public abstract class DatabaseFullPrunedBlockStore implements FullPrunedBlockSto Coin value = Coin.valueOf(results.getLong(2)); byte[] scriptBytes = results.getBytes(3); boolean coinbase = results.getBoolean(4); - StoredTransactionOutput txout = new StoredTransactionOutput(hash, index, value, height, coinbase, scriptBytes); + String address = results.getString(5); + int addressType = results.getInt(6); + UTXO txout = new UTXO(hash, + index, + value, + height, + coinbase, + scriptBytes, + address, + addressType); return txout; } catch (SQLException ex) { throw new BlockStoreException(ex); @@ -955,42 +954,9 @@ public abstract class DatabaseFullPrunedBlockStore implements FullPrunedBlockSto } @Override - public void addUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException { + public void addUnspentTransactionOutput(UTXO out) throws BlockStoreException { maybeConnect(); PreparedStatement s = null; - - // Calculate the toAddress (if any) - String dbAddress = ""; - int type = 0; - Script outputScript = null; - try { - outputScript = new Script(out.getScriptBytes()); - } - catch (ScriptException e) { - // Unparseable, but this isn't an error - it's an output not containing an address - log.info("Could not parse script for output: " + out.getHash().toString()); - } - if (outputScript != null && (outputScript.isSentToAddress() - || outputScript.isSentToRawPubKey() - || outputScript.isPayToScriptHash())) { - if (outputScript.isSentToAddress()) { - Address targetAddr = new Address(params, outputScript.getPubKeyHash()); - dbAddress = targetAddr.toString(); - type = 1; - } - else if (outputScript.isSentToRawPubKey()) { - /* - * Note we use the deprecated getFromAddress here. Coinbase outputs seem to have the target address - * in the pubkey of the script - perhaps we can rename this function? - */ - dbAddress = outputScript.getFromAddress(params).toString(); - type = 2; - } else { - dbAddress = Address.fromP2SHHash(params, outputScript.getPubKeyHash()).toString(); - type = 3; - } - } - try { s = conn.get().prepareStatement(getInsertOpenoutputsSQL()); s.setBytes(1, out.getHash().getBytes()); @@ -999,8 +965,8 @@ public abstract class DatabaseFullPrunedBlockStore implements FullPrunedBlockSto s.setInt(3, out.getHeight()); s.setLong(4, out.getValue().value); s.setBytes(5, out.getScriptBytes()); - s.setString(6, dbAddress); - s.setInt(7, type); + s.setString(6, out.getAddress()); + s.setInt(7, out.getAddressType()); s.setBoolean(8, out.isCoinbase()); s.executeUpdate(); s.close(); @@ -1019,11 +985,11 @@ public abstract class DatabaseFullPrunedBlockStore implements FullPrunedBlockSto } @Override - public void removeUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException { + public void removeUnspentTransactionOutput(UTXO out) throws BlockStoreException { maybeConnect(); // TODO: This should only need one query (maybe a stored procedure) if (getTransactionOutput(out.getHash(), out.getIndex()) == null) - throw new BlockStoreException("Tried to remove a StoredTransactionOutput from DatabaseFullPrunedBlockStore that it didn't have!"); + throw new BlockStoreException("Tried to remove a UTXO from DatabaseFullPrunedBlockStore that it didn't have!"); try { PreparedStatement s = conn.get() .prepareStatement(getDeleteOpenoutputsSQL()); @@ -1105,6 +1071,20 @@ public abstract class DatabaseFullPrunedBlockStore implements FullPrunedBlockSto } } + @Override + public NetworkParameters getParams() { + return params; + } + + @Override + public int getChainHeadHeight() throws UTXOProviderException { + try { + return getVerifiedChainHead().getHeight(); + } catch (BlockStoreException e) { + throw new UTXOProviderException(e); + } + } + /** * Resets the store by deleting the contents of the tables and reinitialising them. * @throws BlockStoreException If the tables couldn't be cleared and initialised. @@ -1147,12 +1127,11 @@ public abstract class DatabaseFullPrunedBlockStore implements FullPrunedBlockSto * the all the openoutputs as stored in the DB (binary), then use calculateClientSide=true

* * @param address The address to calculate the balance of - * @param calculateClientSide Flag to indicate if the DB returns the raw value/s or the actual balance (summed) * @return The balance of the address supplied. If the address has not been seen, or there are no outputs open for this * address, the return value is 0. * @throws BlockStoreException If there is an error getting the balance. */ - protected BigInteger calculateBalanceForAddress(Address address, boolean calculateClientSide) throws BlockStoreException { + protected BigInteger calculateBalanceForAddress(Address address) throws BlockStoreException { maybeConnect(); PreparedStatement s = null; try { @@ -1161,14 +1140,7 @@ public abstract class DatabaseFullPrunedBlockStore implements FullPrunedBlockSto ResultSet rs = s.executeQuery(); BigInteger balance = BigInteger.ZERO; while (rs.next()) { - if(calculateClientSide) { - // The binary data is returned and we calculate the balance. This could be because the DB - // doesn't offer a convert function. - //balance = balance.add(BigInteger.valueOf(rs.getLong(1))); - balance = balance.add(new BigInteger(rs.getBytes(1))); - } else { - return BigInteger.valueOf(rs.getLong(1)); - } + return BigInteger.valueOf(rs.getLong(1)); } return balance; } catch (SQLException ex) { @@ -1184,100 +1156,48 @@ public abstract class DatabaseFullPrunedBlockStore implements FullPrunedBlockSto } } - /** - * Calculate the balance for a coinbase, to-address, or p2sh address. - * - *

The balance {@link org.bitcoinj.store.DatabaseFullPrunedBlockStore#getBalanceSelectSQL()} returns - * the balance (summed) as an number.

- * - * @param address The address to calculate the balance of - * @return The balance of the address supplied. If the address has not been seen, or there are no outputs open for this - * address, the return value is 0. - * @throws BlockStoreException If there is an error getting the balance. - */ - public BigInteger calculateBalanceForAddress(Address address) throws BlockStoreException { - return calculateBalanceForAddress(address, false); - } - - /** - * Get the list of {@link org.bitcoinj.core.TransactionOutput}'s for a given address. - * @param address The address. - * @return The list of transaction outputs. - * @throws BlockStoreException If there is an error getting the list of outputs. - */ - public List getOpenTransactionOutputs(Address address) throws BlockStoreException { - return getOpenTransactionOutputs(address, null); - } - - /** - * Get the list of {@link org.bitcoinj.core.TransactionOutput}'s for a given address and a specified height. - * @param address The address. - * @param maxHeight The maximum block height of this tx output (inclusive). - * @return The list of transaction outputs. - * @throws BlockStoreException If there is an error getting the list of outputs. - */ - public List getOpenTransactionOutputs(Address address, @Nullable Integer maxHeight) throws BlockStoreException { - List outputs = new ArrayList(); - for(List outputsSubList : getOpenTransactionOutputsHeightMap(address, maxHeight).values()) { - outputs.addAll(outputsSubList); - } - return outputs; - } - - /** - * Get the map of the {@link org.bitcoinj.core.TransactionOutput}'s keyed on their height for a given address. - * @param address The address. - * @return The map of transaction outputs (list) keyed by height - * @throws BlockStoreException If there is an error getting the list out outputs. - */ - public Map> getOpenTransactionOutputsHeightMap(Address address) throws BlockStoreException { - return getOpenTransactionOutputsHeightMap(address, null); - } - - /** - * Get the map of the {@link org.bitcoinj.core.TransactionOutput}'s keyed on their height for a given address - * and a specified height. - * @param address The address. - * @param maxHeight The minimum block height of this tx output (inclusive). - * @return The map of transaction outputs (list) keyed by height - * @throws BlockStoreException If there is an error getting the list out outputs. - */ - public Map> getOpenTransactionOutputsHeightMap(Address address, @Nullable Integer maxHeight) throws BlockStoreException { - maybeConnect(); + @Override + public List getOpenTransactionOutputs(List
addresses) throws UTXOProviderException { PreparedStatement s = null; - HashMap> outputsMap = new HashMap>(); + List outputs = new ArrayList(); try { - if(maxHeight != null) { - s = conn.get().prepareStatement(getTrasactionOutputWithHeightSelectSQL()); - s.setInt(2, maxHeight); - } else { - s = conn.get().prepareStatement(getTrasactionOutputSelectSQL()); - } - s.setString(1, address.toString()); - ResultSet rs = s.executeQuery(); - while (rs.next()) { - Coin amount = Coin.valueOf(rs.getLong(1)); - byte[] scriptBytes = rs.getBytes(2); - int height = rs.getInt(3); - TransactionOutput output = new TransactionOutput(params, null, amount, scriptBytes); - if (outputsMap.containsKey(height)) { - outputsMap.get(height).add(output); - } else { - List outputs = new ArrayList(); + maybeConnect(); + s = conn.get().prepareStatement(getTrasactionOutputSelectSQL()); + for (Address address : addresses) { + s.setString(1, address.toString()); + ResultSet rs = s.executeQuery(); + while (rs.next()) { + Sha256Hash hash = new Sha256Hash(rs.getBytes(1)); + Coin amount = Coin.valueOf(rs.getLong(2)); + byte[] scriptBytes = rs.getBytes(3); + int height = rs.getInt(4); + int index = rs.getInt(5); + boolean coinbase = rs.getBoolean(6); + String toAddress = rs.getString(7); + int addressType = rs.getInt(8); + UTXO output = new UTXO(hash, + index, + amount, + height, + coinbase, + scriptBytes, + toAddress, + addressType); outputs.add(output); - outputsMap.put(height, outputs); } } - return outputsMap; + return outputs; } catch (SQLException ex) { - throw new BlockStoreException(ex); + throw new UTXOProviderException(ex); + } catch (BlockStoreException bse) { + throw new UTXOProviderException(bse); } finally { if (s != null) try { s.close(); } catch (SQLException e) { - throw new BlockStoreException("Could not close statement"); - } + throw new UTXOProviderException("Could not close statement", e); + } } } diff --git a/core/src/main/java/org/bitcoinj/store/FullPrunedBlockStore.java b/core/src/main/java/org/bitcoinj/store/FullPrunedBlockStore.java index 5a58ad48..d9b39a1d 100644 --- a/core/src/main/java/org/bitcoinj/store/FullPrunedBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/FullPrunedBlockStore.java @@ -16,10 +16,8 @@ package org.bitcoinj.store; -import org.bitcoinj.core.Sha256Hash; -import org.bitcoinj.core.StoredBlock; -import org.bitcoinj.core.StoredTransactionOutput; -import org.bitcoinj.core.StoredUndoableBlock; +import org.bitcoinj.core.*; + /** *

An implementor of FullPrunedBlockStore saves StoredBlock objects to some storage mechanism.

@@ -43,12 +41,12 @@ import org.bitcoinj.core.StoredUndoableBlock; *

A FullPrunedBlockStore contains a map of hashes to [Full]StoredBlock. The hash is the double digest of the * Bitcoin serialization of the block header, not the header with the extra data as well.

* - *

A FullPrunedBlockStore also contains a map of hash+index to StoredTransactionOutput. Again, the hash is + *

A FullPrunedBlockStore also contains a map of hash+index to UTXO. Again, the hash is * a standard Bitcoin double-SHA256 hash of the transaction.

* *

FullPrunedBlockStores are thread safe.

*/ -public interface FullPrunedBlockStore extends BlockStore { +public interface FullPrunedBlockStore extends BlockStore, UTXOProvider { /** *

Saves the given {@link StoredUndoableBlock} and {@link StoredBlock}. Calculates keys from the {@link StoredBlock}

* @@ -74,21 +72,21 @@ public interface FullPrunedBlockStore extends BlockStore { StoredUndoableBlock getUndoBlock(Sha256Hash hash) throws BlockStoreException; /** - * Gets a {@link StoredTransactionOutput} with the given hash and index, or null if none is found + * Gets a {@link org.bitcoinj.core.UTXO} with the given hash and index, or null if none is found */ - StoredTransactionOutput getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException; + UTXO getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException; /** - * Adds a {@link StoredTransactionOutput} to the list of unspent TransactionOutputs + * Adds a {@link org.bitcoinj.core.UTXO} to the list of unspent TransactionOutputs */ - void addUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException; + void addUnspentTransactionOutput(UTXO out) throws BlockStoreException; /** - * Removes a {@link StoredTransactionOutput} from the list of unspent TransactionOutputs + * Removes a {@link org.bitcoinj.core.UTXO} from the list of unspent TransactionOutputs * Note that the coinbase of the genesis block should NEVER be spendable and thus never in the list. * @throws BlockStoreException if there is an underlying storage issue, or out was not in the list. */ - void removeUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException; + void removeUnspentTransactionOutput(UTXO out) throws BlockStoreException; /** * True if this store has any unspent outputs from a transaction with a hash equal to the first parameter @@ -109,7 +107,7 @@ public interface FullPrunedBlockStore extends BlockStore { * before a call to commitDatabaseBatchWrite. * * If chainHead has a greater height than the non-verified chain head (ie that set with - * {@link BlockStore.setChainHead}) the non-verified chain head should be set to the one set here. + * {@link BlockStore#setChainHead}) the non-verified chain head should be set to the one set here. * In this way a class using a FullPrunedBlockStore only in full-verification mode can ignore the regular * {@link BlockStore} functions implemented as a part of a FullPrunedBlockStore. */ diff --git a/core/src/main/java/org/bitcoinj/store/MemoryBlockStore.java b/core/src/main/java/org/bitcoinj/store/MemoryBlockStore.java index cf3d891c..0ea4f4d1 100644 --- a/core/src/main/java/org/bitcoinj/store/MemoryBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/MemoryBlockStore.java @@ -32,6 +32,7 @@ public class MemoryBlockStore implements BlockStore { } }; private StoredBlock chainHead; + private NetworkParameters params; public MemoryBlockStore(NetworkParameters params) { // Insert the genesis block. @@ -40,6 +41,7 @@ public class MemoryBlockStore implements BlockStore { StoredBlock storedGenesis = new StoredBlock(genesisHeader, genesisHeader.getWork(), 0); put(storedGenesis); setChainHead(storedGenesis); + this.params = params; } catch (BlockStoreException e) { throw new RuntimeException(e); // Cannot happen. } catch (VerificationException e) { @@ -76,4 +78,9 @@ public class MemoryBlockStore implements BlockStore { public void close() { blockMap = null; } + + @Override + public NetworkParameters getParams() { + return params; + } } diff --git a/core/src/main/java/org/bitcoinj/store/MemoryFullPrunedBlockStore.java b/core/src/main/java/org/bitcoinj/store/MemoryFullPrunedBlockStore.java index f86a13d2..332cb428 100644 --- a/core/src/main/java/org/bitcoinj/store/MemoryFullPrunedBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/MemoryFullPrunedBlockStore.java @@ -42,7 +42,7 @@ class StoredTransactionOutPoint implements Serializable { this.index = index; } - StoredTransactionOutPoint(StoredTransactionOutput out) { + StoredTransactionOutPoint(UTXO out) { this.hash = out.getHash(); this.index = out.getIndex(); } @@ -132,6 +132,14 @@ class TransactionalHashMap { } return map.get(key); } + + public List values() { + List valueTypes = new ArrayList(); + for (KeyType keyType : map.keySet()) { + valueTypes.add(get(keyType)); + } + return valueTypes; + } public void put(KeyType key, ValueType value) { if (Boolean.TRUE.equals(inTransaction.get())) { @@ -224,10 +232,10 @@ class TransactionalMultiKeyHashMap { } /** - * Keeps {@link StoredBlock}s, {@link StoredUndoableBlock}s and {@link StoredTransactionOutput}s in memory. + * Keeps {@link StoredBlock}s, {@link StoredUndoableBlock}s and {@link org.bitcoinj.core.UTXO}s in memory. * Used primarily for unit testing. */ -public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore { +public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore, UTXOProvider { protected static class StoredBlockAndWasUndoableFlag { public StoredBlock block; public boolean wasUndoable; @@ -236,10 +244,11 @@ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore { private TransactionalHashMap blockMap; private TransactionalMultiKeyHashMap fullBlockMap; //TODO: Use something more suited to remove-heavy use? - private TransactionalHashMap transactionOutputMap; + private TransactionalHashMap transactionOutputMap; private StoredBlock chainHead; private StoredBlock verifiedChainHead; private int fullStoreDepth; + private NetworkParameters params; /** * Set up the MemoryFullPrunedBlockStore @@ -249,7 +258,7 @@ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore { public MemoryFullPrunedBlockStore(NetworkParameters params, int fullStoreDepth) { blockMap = new TransactionalHashMap(); fullBlockMap = new TransactionalMultiKeyHashMap(); - transactionOutputMap = new TransactionalHashMap(); + transactionOutputMap = new TransactionalHashMap(); this.fullStoreDepth = fullStoreDepth > 0 ? fullStoreDepth : 1; // Insert the genesis block. try { @@ -260,6 +269,7 @@ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore { put(storedGenesisHeader, storedGenesis); setChainHead(storedGenesisHeader); setVerifiedChainHead(storedGenesisHeader); + this.params = params; } catch (BlockStoreException e) { throw new RuntimeException(e); // Cannot happen. } catch (VerificationException e) { @@ -343,22 +353,22 @@ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore { @Override @Nullable - public synchronized StoredTransactionOutput getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException { + public synchronized UTXO getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException { Preconditions.checkNotNull(transactionOutputMap, "MemoryFullPrunedBlockStore is closed"); return transactionOutputMap.get(new StoredTransactionOutPoint(hash, index)); } @Override - public synchronized void addUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException { + public synchronized void addUnspentTransactionOutput(UTXO out) throws BlockStoreException { Preconditions.checkNotNull(transactionOutputMap, "MemoryFullPrunedBlockStore is closed"); transactionOutputMap.put(new StoredTransactionOutPoint(out), out); } @Override - public synchronized void removeUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException { + public synchronized void removeUnspentTransactionOutput(UTXO out) throws BlockStoreException { Preconditions.checkNotNull(transactionOutputMap, "MemoryFullPrunedBlockStore is closed"); if (transactionOutputMap.remove(new StoredTransactionOutPoint(out)) == null) - throw new BlockStoreException("Tried to remove a StoredTransactionOutput from MemoryFullPrunedBlockStore that it didn't have!"); + throw new BlockStoreException("Tried to remove a UTXO from MemoryFullPrunedBlockStore that it didn't have!"); } @Override @@ -389,4 +399,34 @@ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore { return true; return false; } + + @Override + public NetworkParameters getParams() { + return params; + } + + @Override + public int getChainHeadHeight() throws UTXOProviderException { + try { + return getVerifiedChainHead().getHeight(); + } catch (BlockStoreException e) { + throw new UTXOProviderException(e); + } + } + + @Override + public List getOpenTransactionOutputs(List
addresses) throws UTXOProviderException { + // This is *NOT* optimal: We go through all the outputs and select the ones we are looking for. + // If someone uses this store for production then they have a lot more to worry about than an inefficient impl :) + List foundOutputs = new ArrayList(); + List outputsList = transactionOutputMap.values(); + for (UTXO output : outputsList) { + for (Address address : addresses) { + if (output.getAddress().equals(address.toString())) { + foundOutputs.add(output); + } + } + } + return foundOutputs; + } } diff --git a/core/src/main/java/org/bitcoinj/store/MySQLFullPrunedBlockStore.java b/core/src/main/java/org/bitcoinj/store/MySQLFullPrunedBlockStore.java index e240fef7..07d4a7c8 100644 --- a/core/src/main/java/org/bitcoinj/store/MySQLFullPrunedBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/MySQLFullPrunedBlockStore.java @@ -55,7 +55,7 @@ public class MySQLFullPrunedBlockStore extends DatabaseFullPrunedBlockStore { " hash blob NOT NULL,\n" + " height integer NOT NULL,\n" + " txoutchanges mediumblob,\n" + - " transactions blob,\n" + + " transactions mediumblob,\n" + " CONSTRAINT `undoableblocks_pk` PRIMARY KEY (hash(28)) USING btree\n" + ")\n"; @@ -72,16 +72,18 @@ public class MySQLFullPrunedBlockStore extends DatabaseFullPrunedBlockStore { ")\n"; // Some indexes to speed up inserts - private static final String CREATE_OUTPUTS_ADDRESS_MULTI_INDEX = "CREATE INDEX openoutputs_hash_index_height_toaddress_idx ON openoutputs (hash(32), `index`, height, toaddress) USING btree"; - private static final String CREATE_OUTPUTS_TOADDRESS_INDEX = "CREATE INDEX openoutputs_toaddress_idx ON openoutputs (toaddress) USING btree"; - private static final String CREATE_OUTPUTS_ADDRESSTARGETABLE_INDEX = "CREATE INDEX openoutputs_addresstargetable_idx ON openoutputs (addresstargetable) USING btree"; - private static final String CREATE_OUTPUTS_HASH_INDEX = "CREATE INDEX openoutputs_hash_idx ON openoutputs (hash(32)) USING btree"; - private static final String CREATE_UNDOABLE_TABLE_INDEX = "CREATE INDEX undoableblocks_height_idx ON undoableBlocks (height) USING btree"; + private static final String CREATE_OUTPUTS_ADDRESS_MULTI_INDEX = "CREATE INDEX openoutputs_hash_index_height_toaddress_idx ON openoutputs (hash(32), `index`, height, toaddress) USING btree"; + private static final String CREATE_OUTPUTS_TOADDRESS_INDEX = "CREATE INDEX openoutputs_toaddress_idx ON openoutputs (toaddress) USING btree"; + private static final String CREATE_OUTPUTS_ADDRESSTARGETABLE_INDEX = "CREATE INDEX openoutputs_addresstargetable_idx ON openoutputs (addresstargetable) USING btree"; + private static final String CREATE_OUTPUTS_HASH_INDEX = "CREATE INDEX openoutputs_hash_idx ON openoutputs (hash(32)) USING btree"; + private static final String CREATE_UNDOABLE_TABLE_INDEX = "CREATE INDEX undoableblocks_height_idx ON undoableBlocks (height) USING btree"; // SQL involving index column (table openOutputs) overridden as it is a reserved word and must be back ticked in MySQL. - private static final String SELECT_OPENOUTPUTS_SQL = "SELECT height, value, scriptBytes, coinbase FROM openOutputs WHERE hash = ? AND `index` = ?"; - private static final String INSERT_OPENOUTPUTS_SQL = "INSERT INTO openOutputs (hash, `index`, height, value, scriptBytes, toAddress, addressTargetable, coinbase) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; - private static final String DELETE_OPENOUTPUTS_SQL = "DELETE FROM openOutputs WHERE hash = ? AND `index` = ?"; + private static final String SELECT_OPENOUTPUTS_SQL = "SELECT height, value, scriptBytes, coinbase, toaddress, addresstargetable FROM openOutputs WHERE hash = ? AND `index` = ?"; + private static final String INSERT_OPENOUTPUTS_SQL = "INSERT INTO openOutputs (hash, `index`, height, value, scriptBytes, toAddress, addressTargetable, coinbase) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + private static final String DELETE_OPENOUTPUTS_SQL = "DELETE FROM openOutputs WHERE hash = ? AND `index` = ?"; + + private static final String SELECT_TRANSACTION_OUTPUTS_SQL = "SELECT hash, value, scriptBytes, height, `index`, coinbase, toaddress, addresstargetable FROM openOutputs where toaddress = ?"; /** * Creates a new MySQLFullPrunedBlockStore. @@ -119,6 +121,11 @@ public class MySQLFullPrunedBlockStore extends DatabaseFullPrunedBlockStore { return DELETE_OPENOUTPUTS_SQL; } + @Override + protected String getTrasactionOutputSelectSQL() { + return SELECT_TRANSACTION_OUTPUTS_SQL; + } + @Override protected List getCreateTablesSQL() { List sqlStatements = new ArrayList(); diff --git a/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java b/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java index 661b07d5..38634aad 100644 --- a/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/SPVBlockStore.java @@ -276,6 +276,11 @@ public class SPVBlockStore implements BlockStore { } } + @Override + public NetworkParameters getParams() { + return params; + } + protected static final int RECORD_SIZE = 32 /* hash */ + StoredBlock.COMPACT_SERIALIZED_SIZE; // File format: diff --git a/core/src/main/java/org/bitcoinj/wallet/CoinSelection.java b/core/src/main/java/org/bitcoinj/wallet/CoinSelection.java index ff4fba53..d9e41c4a 100644 --- a/core/src/main/java/org/bitcoinj/wallet/CoinSelection.java +++ b/core/src/main/java/org/bitcoinj/wallet/CoinSelection.java @@ -7,7 +7,7 @@ import java.util.Collection; /** * Represents the results of a - * {@link CoinSelector#select(Coin, java.util.LinkedList)} operation. A + * {@link CoinSelector#select(Coin, java.util.List)} operation. A * coin selection represents a list of spendable transaction outputs that sum together to give valueGathered. * Different coin selections could be produced by different coin selectors from the same input set, according * to their varying policies. diff --git a/core/src/main/java/org/bitcoinj/wallet/DefaultCoinSelector.java b/core/src/main/java/org/bitcoinj/wallet/DefaultCoinSelector.java index e5bff5bf..c8a593ca 100644 --- a/core/src/main/java/org/bitcoinj/wallet/DefaultCoinSelector.java +++ b/core/src/main/java/org/bitcoinj/wallet/DefaultCoinSelector.java @@ -48,14 +48,8 @@ public class DefaultCoinSelector implements CoinSelector { Collections.sort(outputs, new Comparator() { @Override public int compare(TransactionOutput a, TransactionOutput b) { - int depth1 = 0; - int depth2 = 0; - TransactionConfidence conf1 = a.getParentTransaction().getConfidence(); - TransactionConfidence conf2 = b.getParentTransaction().getConfidence(); - if (conf1.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) - depth1 = conf1.getDepthInBlocks(); - if (conf2.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) - depth2 = conf2.getDepthInBlocks(); + int depth1 = a.getParentTransactionDepthInBlocks(); + int depth2 = b.getParentTransactionDepthInBlocks(); Coin aValue = a.getValue(); Coin bValue = b.getValue(); BigInteger aCoinDepth = BigInteger.valueOf(aValue.value).multiply(BigInteger.valueOf(depth1)); @@ -66,8 +60,8 @@ public class DefaultCoinSelector implements CoinSelector { int c2 = bValue.compareTo(aValue); if (c2 != 0) return c2; // They are entirely equivalent (possibly pending) so sort by hash to ensure a total ordering. - BigInteger aHash = a.getParentTransaction().getHash().toBigInteger(); - BigInteger bHash = b.getParentTransaction().getHash().toBigInteger(); + BigInteger aHash = a.getParentTransactionHash().toBigInteger(); + BigInteger bHash = b.getParentTransactionHash().toBigInteger(); return aHash.compareTo(bHash); } }); @@ -75,7 +69,10 @@ public class DefaultCoinSelector implements CoinSelector { /** Sub-classes can override this to just customize whether transactions are usable, but keep age sorting. */ protected boolean shouldSelect(Transaction tx) { - return isSelectable(tx); + if (tx != null) { + return isSelectable(tx); + } + return true; } public static boolean isSelectable(Transaction tx) { diff --git a/core/src/test/java/org/bitcoinj/core/AbstractFullPrunedBlockChainTest.java b/core/src/test/java/org/bitcoinj/core/AbstractFullPrunedBlockChainTest.java index 7ff39c6f..a88fff39 100644 --- a/core/src/test/java/org/bitcoinj/core/AbstractFullPrunedBlockChainTest.java +++ b/core/src/test/java/org/bitcoinj/core/AbstractFullPrunedBlockChainTest.java @@ -17,6 +17,7 @@ package org.bitcoinj.core; +import com.google.common.collect.Lists; import org.bitcoinj.params.MainNetParams; import org.bitcoinj.params.UnitTestParams; import org.bitcoinj.script.Script; @@ -24,6 +25,7 @@ import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.store.FullPrunedBlockStore; import org.bitcoinj.utils.BlockFileLoader; import org.bitcoinj.utils.BriefLogFormatter; +import org.bitcoinj.wallet.WalletTransaction; import org.junit.Before; import org.junit.Test; import org.slf4j.Logger; @@ -32,6 +34,7 @@ import org.slf4j.LoggerFactory; import java.io.File; import java.lang.ref.WeakReference; import java.util.Arrays; +import java.util.List; import static org.bitcoinj.core.Coin.FIFTY_COINS; import static org.junit.Assert.*; @@ -70,7 +73,6 @@ public abstract class AbstractFullPrunedBlockChainTest RuleList blockList = generator.getBlocksToTest(false, false, null); store = createStore(params, blockList.maximumReorgBlockCount); - resetStore(store); chain = new FullPrunedBlockChain(params, store); for (Rule rule : blockList.list) { @@ -108,12 +110,14 @@ public abstract class AbstractFullPrunedBlockChainTest fail(); } } + try { + store.close(); + } catch (Exception e) {} } @Test public void skipScripts() throws Exception { store = createStore(params, 10); - resetStore(store); chain = new FullPrunedBlockChain(params, store); // Check that we aren't accidentally leaving any references @@ -144,13 +148,15 @@ public abstract class AbstractFullPrunedBlockChainTest } catch (VerificationException e) { fail(); } + try { + store.close(); + } catch (Exception e) {} } @Test public void testFinalizedBlocks() throws Exception { final int UNDOABLE_BLOCKS_STORED = 10; store = createStore(params, UNDOABLE_BLOCKS_STORED); - resetStore(store); chain = new FullPrunedBlockChain(params, store); // Check that we aren't accidentally leaving any references @@ -168,13 +174,13 @@ public abstract class AbstractFullPrunedBlockChainTest chain.add(rollingBlock); } - WeakReference out = new WeakReference + WeakReference out = new WeakReference (store.getTransactionOutput(spendableOutput.getHash(), spendableOutput.getIndex())); rollingBlock = rollingBlock.createNextBlock(null); Transaction t = new Transaction(params); // Entirely invalid scriptPubKey - t.addOutput(new TransactionOutput(params, t, FIFTY_COINS, new byte[] {})); + t.addOutput(new TransactionOutput(params, t, FIFTY_COINS, new byte[]{})); t.addSignedInput(spendableOutput, new Script(spendableOutputScriptPubKey), outKey); rollingBlock.addTransaction(t); rollingBlock.solve(); @@ -199,6 +205,9 @@ public abstract class AbstractFullPrunedBlockChainTest assertNull(undoBlock.get()); assertNull(changes.get()); assertNull(out.get()); + try { + store.close(); + } catch (Exception e) {} } @Test @@ -212,5 +221,121 @@ public abstract class AbstractFullPrunedBlockChainTest chain = new FullPrunedBlockChain(params, store); for (Block block : loader) chain.add(block); + try { + store.close(); + } catch (Exception e) {} + } + + @Test + public void testGetOpenTransactionOutputs() throws Exception { + final int UNDOABLE_BLOCKS_STORED = 10; + store = createStore(params, UNDOABLE_BLOCKS_STORED); + chain = new FullPrunedBlockChain(params, store); + + // Check that we aren't accidentally leaving any references + // to the full StoredUndoableBlock's lying around (ie memory leaks) + ECKey outKey = new ECKey(); + + // Build some blocks on genesis block to create a spendable output + Block rollingBlock = params.getGenesisBlock().createNextBlockWithCoinbase(outKey.getPubKey()); + chain.add(rollingBlock); + Transaction transaction = rollingBlock.getTransactions().get(0); + TransactionOutPoint spendableOutput = new TransactionOutPoint(params, 0, transaction.getHash()); + byte[] spendableOutputScriptPubKey = transaction.getOutputs().get(0).getScriptBytes(); + for (int i = 1; i < params.getSpendableCoinbaseDepth(); i++) { + rollingBlock = rollingBlock.createNextBlockWithCoinbase(outKey.getPubKey()); + chain.add(rollingBlock); + } + rollingBlock = rollingBlock.createNextBlock(null); + + // Create bitcoin spend of 1 BTC. + ECKey toKey = new ECKey(); + Coin amount = Coin.valueOf(100000000); + Address address = new Address(params, toKey.getPubKeyHash()); + Coin totalAmount = Coin.ZERO; + + Transaction t = new Transaction(params); + t.addOutput(new TransactionOutput(params, t, amount, toKey)); + t.addSignedInput(spendableOutput, new Script(spendableOutputScriptPubKey), outKey); + rollingBlock.addTransaction(t); + rollingBlock.solve(); + chain.add(rollingBlock); + totalAmount = totalAmount.add(amount); + + List outputs = store.getOpenTransactionOutputs(Lists.newArrayList(address)); + assertNotNull(outputs); + assertEquals("Wrong Number of Outputs", 1, outputs.size()); + UTXO output = outputs.get(0); + assertEquals("The address is not equal", address.toString(), output.getAddress()); + assertEquals("The amount is not equal", totalAmount, output.getValue()); + + outputs = null; + output = null; + try { + store.close(); + } catch (Exception e) {} + } + + @Test + public void testUTXOProviderWithWallet() throws Exception { + final int UNDOABLE_BLOCKS_STORED = 10; + store = createStore(params, UNDOABLE_BLOCKS_STORED); + chain = new FullPrunedBlockChain(params, store); + + // Check that we aren't accidentally leaving any references + // to the full StoredUndoableBlock's lying around (ie memory leaks) + ECKey outKey = new ECKey(); + + // Build some blocks on genesis block to create a spendable output. + Block rollingBlock = params.getGenesisBlock().createNextBlockWithCoinbase(outKey.getPubKey()); + chain.add(rollingBlock); + Transaction transaction = rollingBlock.getTransactions().get(0); + TransactionOutPoint spendableOutput = new TransactionOutPoint(params, 0, transaction.getHash()); + byte[] spendableOutputScriptPubKey = transaction.getOutputs().get(0).getScriptBytes(); + for (int i = 1; i < params.getSpendableCoinbaseDepth(); i++) { + rollingBlock = rollingBlock.createNextBlockWithCoinbase(outKey.getPubKey()); + chain.add(rollingBlock); + } + rollingBlock = rollingBlock.createNextBlock(null); + + // Create 1 BTC spend to a key in this wallet (to ourselves). + Wallet wallet = new Wallet(params); + assertEquals("Available balance is incorrect", Coin.ZERO, wallet.getBalance(Wallet.BalanceType.AVAILABLE)); + assertEquals("Estimated balance is incorrect", Coin.ZERO, wallet.getBalance(Wallet.BalanceType.ESTIMATED)); + + wallet.setUTXOProvider(store); + ECKey toKey = wallet.freshReceiveKey(); + Coin amount = Coin.valueOf(100000000); + + Transaction t = new Transaction(params); + t.addOutput(new TransactionOutput(params, t, amount, toKey)); + t.addSignedInput(spendableOutput, new Script(spendableOutputScriptPubKey), outKey); + rollingBlock.addTransaction(t); + rollingBlock.solve(); + chain.add(rollingBlock); + + // Create another spend of 1/2 the value of BTC we have available using the wallet (store coin selector). + ECKey toKey2 = new ECKey(); + Coin amount2 = amount.divide(2); + Address address2 = new Address(params, toKey2.getPubKeyHash()); + Wallet.SendRequest req = Wallet.SendRequest.to(address2, amount2); + wallet.completeTx(req); + wallet.commitTx(req.tx); + Coin fee = req.fee; + + // There should be one pending tx (our spend). + assertEquals("Wrong number of PENDING.4", 1, wallet.getPoolSize(WalletTransaction.Pool.PENDING)); + Coin totalPendingTxAmount = Coin.ZERO; + for (Transaction tx : wallet.getPendingTransactions()) { + totalPendingTxAmount = totalPendingTxAmount.add(tx.getValueSentToMe(wallet)); + } + + // The availbale balance should be the 0 (as we spent the 1 BTC that's pending) and estimated should be 1/2 - fee BTC + assertEquals("Available balance is incorrect", Coin.ZERO, wallet.getBalance(Wallet.BalanceType.AVAILABLE)); + assertEquals("Estimated balance is incorrect", amount2.subtract(fee), wallet.getBalance(Wallet.BalanceType.ESTIMATED)); + assertEquals("Pending tx amount is incorrect", amount2.subtract(fee), totalPendingTxAmount); + try { + store.close(); + } catch (Exception e) {} } } diff --git a/core/src/test/java/org/bitcoinj/core/H2FullPrunedBlockChainTest.java b/core/src/test/java/org/bitcoinj/core/H2FullPrunedBlockChainTest.java index 96b14d25..2ff7dc4c 100644 --- a/core/src/test/java/org/bitcoinj/core/H2FullPrunedBlockChainTest.java +++ b/core/src/test/java/org/bitcoinj/core/H2FullPrunedBlockChainTest.java @@ -25,6 +25,7 @@ public class H2FullPrunedBlockChainTest extends AbstractFullPrunedBlockChainTest private void deleteFiles() { maybeDelete("test.h2.db"); maybeDelete("test.trace.db"); + maybeDelete("test.lock.db"); } private void maybeDelete(String s) {