From 80f141cbf54e7d9eb4aa6cdfa65a597805c30dc3 Mon Sep 17 00:00:00 2001 From: Jim Burton Date: Mon, 21 May 2012 15:18:49 +0100 Subject: [PATCH] TransactionConfidence changes (coinbase phase 2) + Mike's feedback --- core/src/bitcoin.proto | 8 + .../com/google/bitcoin/core/BlockChain.java | 2 +- .../com/google/bitcoin/core/Transaction.java | 22 ++- .../bitcoin/core/TransactionConfidence.java | 68 +++++--- .../java/com/google/bitcoin/core/Wallet.java | 106 +++++++++--- .../store/WalletProtobufSerializer.java | 18 ++ .../main/java/org/bitcoinj/wallet/Protos.java | 159 +++++++++++++++--- ...ainSplitTests.java => ChainSplitTest.java} | 122 +++++++++++--- .../com/google/bitcoin/core/WalletTest.java | 1 - .../store/WalletProtobufSerializerTest.java | 82 ++++++++- .../google/bitcoin/examples/PingService.java | 10 +- 11 files changed, 487 insertions(+), 111 deletions(-) rename core/src/test/java/com/google/bitcoin/core/{ChainSplitTests.java => ChainSplitTest.java} (79%) diff --git a/core/src/bitcoin.proto b/core/src/bitcoin.proto index 42d2f3e4..328aab5c 100644 --- a/core/src/bitcoin.proto +++ b/core/src/bitcoin.proto @@ -100,6 +100,14 @@ message TransactionConfidence { // multiple transactions in the case of several inputs being re-spent by several transactions but we don't // bother to track them all, just the first. This only makes sense if type = OVERRIDDEN_BY_DOUBLE_SPEND. optional bytes overriding_transaction = 3; + + // If type == BUILDING then this is the depth of the transaction in the blockchain. + // Zero confirmations: depth = 0, one confirmation: depth = 1 etc. + optional int32 depth = 4; + + // If type == BUILDING then this is the cumulative workDone for the block the transaction appears in, together with + // all blocks that bury it. + optional int64 work_done = 5; } /** A bitcoin transaction */ diff --git a/core/src/main/java/com/google/bitcoin/core/BlockChain.java b/core/src/main/java/com/google/bitcoin/core/BlockChain.java index 54bb2aad..3647e0e5 100644 --- a/core/src/main/java/com/google/bitcoin/core/BlockChain.java +++ b/core/src/main/java/com/google/bitcoin/core/BlockChain.java @@ -290,7 +290,7 @@ public class BlockChain { // can be updated to take into account the re-organize. We might also have received new coins we didn't have // before and our previous spends might have been undone. for (Wallet wallet : wallets) { - wallet.reorganize(oldBlocks, newBlocks); + wallet.reorganize(splitPoint, oldBlocks, newBlocks); } // Update the pointer to the best known block. setChainHead(newChainHead); 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 b262d3d7..c2c807b1 100644 --- a/core/src/main/java/com/google/bitcoin/core/Transaction.java +++ b/core/src/main/java/com/google/bitcoin/core/Transaction.java @@ -16,6 +16,7 @@ package com.google.bitcoin.core; +import com.google.bitcoin.core.TransactionConfidence.ConfidenceType; import com.google.common.base.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -259,13 +260,23 @@ public class Transaction extends ChildMessage implements Serializable { if (bestChain && (updatedAt == null || updatedAt.getTime() == 0 || updatedAt.getTime() > blockTime)) { updatedAt = new Date(blockTime); } - + addBlockAppearance(block.getHeader().getHash()); if (bestChain) { - // This can cause event listeners on TransactionConfidence to run. After this line completes, the wallets + // This can cause event listeners on TransactionConfidence to run. After these lines complete, the wallets // state may have changed! - getConfidence().setAppearedAtChainHeight(block.getHeight()); + TransactionConfidence transactionConfidence = getConfidence(); + transactionConfidence.setAppearedAtChainHeight(block.getHeight()); + + // Reset the confidence block depth. + transactionConfidence.setDepthInBlocks(0); + + // Reset the work done. + transactionConfidence.setWorkDone(BigInteger.ZERO); + + // The transaction is now on the best chain. + transactionConfidence.setConfidenceType(ConfidenceType.BUILDING); } } @@ -278,7 +289,10 @@ public class Transaction extends ChildMessage implements Serializable { /** Called by the wallet once a re-org means we don't appear in the best chain anymore. */ void notifyNotOnBestChain() { - getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.NOT_IN_BEST_CHAIN); + TransactionConfidence transactionConfidence = getConfidence(); + transactionConfidence.setConfidenceType(TransactionConfidence.ConfidenceType.NOT_IN_BEST_CHAIN); + transactionConfidence.setDepthInBlocks(0); + transactionConfidence.setWorkDone(BigInteger.ZERO); } /** diff --git a/core/src/main/java/com/google/bitcoin/core/TransactionConfidence.java b/core/src/main/java/com/google/bitcoin/core/TransactionConfidence.java index 55c07100..28c9261e 100644 --- a/core/src/main/java/com/google/bitcoin/core/TransactionConfidence.java +++ b/core/src/main/java/com/google/bitcoin/core/TransactionConfidence.java @@ -16,7 +16,6 @@ package com.google.bitcoin.core; -import com.google.bitcoin.store.BlockStoreException; import com.google.bitcoin.utils.EventListenerInvoker; import com.google.common.base.Preconditions; @@ -50,11 +49,9 @@ import java.util.Set; *

Alternatively, you may know that the transaction is "dead", that is, one or more of its inputs have * been double spent and will never confirm unless there is another re-org.

* - *

TransactionConfidence is purely a data structure, it doesn't try and keep itself up to date. To have fresh - * confidence data, you need to ensure the owning {@link Transaction} is being updated by something, like - * a {@link Wallet}.

Confidence objects are live and can be updated by other threads running in parallel - * to your own. To make a copy that won't be changed, use - * {@link com.google.bitcoin.core.TransactionConfidence#duplicate()}. + *

TransactionConfidence is updated via the {@link com.google.bitcoin.core.TransactionConfidence#notifyWorkDone()} + * method to ensure the block depth and work done are up to date.

+ * To make a copy that won't be changed, use {@link com.google.bitcoin.core.TransactionConfidence#duplicate()}. */ public class TransactionConfidence implements Serializable { private static final long serialVersionUID = 4577920141400556444L; @@ -70,6 +67,18 @@ public class TransactionConfidence implements Serializable { // Lazily created listeners array. private transient ArrayList listeners; + /** + * The depth of the transaction on the best chain in blocks. An unconfirmed block has depth 0, after one confirmation + * its depth is 1. + */ + private int depth; + + /** + * The cumulative work done for the blocks that bury this transaction. BigInteger.ZERO if the transaction is not + * on the best chain. + */ + private BigInteger workDone = BigInteger.ZERO; + // TODO: The advice below is a mess. There should be block chain listeners, see issue 94. /** *

Adds an event listener that will be run when this confidence object is updated. The listener will be locked and @@ -278,6 +287,18 @@ public class TransactionConfidence implements Serializable { return builder.toString(); } + /** + * Called by the wallet when the tx appears on the best chain and a new block is added to the top. + * Updates the internal counter that tracks how deeply buried the block is. + * Work is the value of block.getWork(). + */ + public synchronized void notifyWorkDone(Block block) throws VerificationException { + if (getConfidenceType() == ConfidenceType.BUILDING) { + this.depth++; + this.workDone = this.workDone.add(block.getWork()); + } + } + /** * Depth in the chain is an approximation of how much time has elapsed since the transaction has been confirmed. On * average there is supposed to be a new block every 10 minutes, but the actual rate may vary. The reference @@ -289,16 +310,21 @@ public class TransactionConfidence implements Serializable { * chain yet, throws IllegalStateException, so use {@link com.google.bitcoin.core.TransactionConfidence#getConfidenceType()} * to check first. * - * @param chain a {@link BlockChain} instance. * @throws IllegalStateException if confidence type != BUILDING. * @return depth */ - public synchronized int getDepthInBlocks(BlockChain chain) { + public synchronized int getDepthInBlocks() { if (getConfidenceType() != ConfidenceType.BUILDING) { throw new IllegalStateException("Confidence type is not BUILDING"); } - int height = getAppearedAtChainHeight(); - return chain.getBestChainHeight() - height + 1; + return depth; + } + + /* + * Set the depth in blocks. Having one block confirmation is a depth of one. + */ + public synchronized void setDepthInBlocks(int depth) { + this.depth = depth; } /** @@ -307,24 +333,18 @@ public class TransactionConfidence implements Serializable { * blocks per hour by adjusting the difficulty target. So to know how much real computation effort is needed to * reverse a transaction, counting blocks is not enough. * - * @param chain * @throws IllegalStateException if confidence type is not BUILDING * @return estimated number of hashes needed to reverse the transaction. */ - public synchronized BigInteger getWorkDone(BlockChain chain) throws BlockStoreException { - int depth; - synchronized (this) { - if (getConfidenceType() != ConfidenceType.BUILDING) - throw new IllegalStateException("Confidence type is " + getConfidenceType() + ", not BUILDING"); - depth = getDepthInBlocks(chain); + public synchronized BigInteger getWorkDone() { + if (getConfidenceType() != ConfidenceType.BUILDING) { + throw new IllegalStateException("Confidence type is not BUILDING"); } - BigInteger work = BigInteger.ZERO; - StoredBlock block = chain.getChainHead(); - for (; depth > 0; depth--) { - work = work.add(block.getChainWork()); - block = block.getPrev(chain.blockStore); - } - return work; + return workDone; + } + + public synchronized void setWorkDone(BigInteger workDone) { + this.workDone = workDone; } /** diff --git a/core/src/main/java/com/google/bitcoin/core/Wallet.java b/core/src/main/java/com/google/bitcoin/core/Wallet.java index e13d7255..55d4ba85 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -16,6 +16,7 @@ package com.google.bitcoin.core; +import com.google.bitcoin.core.TransactionConfidence.ConfidenceType; import com.google.bitcoin.core.WalletTransaction.Pool; import com.google.bitcoin.store.WalletProtobufSerializer; import com.google.bitcoin.utils.EventListenerInvoker; @@ -551,29 +552,39 @@ public class Wallet implements Serializable { log.info("Balance is now: " + bitcoinValueToFriendlyString(getBalance())); - // Store the block hash - if (bestChain) { - if (block != null && block.getHeader() != null) { - // Check to see if this block has been seen before - Sha256Hash newBlockHash = block.getHeader().getHash(); - if (!newBlockHash.equals(getLastBlockSeenHash())) { - // new hash - setLastBlockSeenHash(newBlockHash); - } - } - } - // WARNING: The code beyond this point can trigger event listeners on transaction confidence objects, which are // in turn allowed to re-enter the Wallet. This means we cannot assume anything about the state of the wallet // from now on. The balance just received may already be spent. - // Mark the tx as appearing in this block so we can find it later after a re-org. This also lets the - // transaction update its confidence and timestamp bookkeeping data. + // Mark the tx as appearing in this block so we can find it later after a re-org. if (block != null) { tx.setBlockAppearance(block, bestChain); - invokeOnTransactionConfidenceChanged(tx); } + // If this is the first time the the block hash has been seen, store it and notify transactions of the block. + if (bestChain) { + if (block != null && block.getHeader() != null) { + // Check to see if this block has been seen before. + Sha256Hash newBlockHash = block.getHeader().getHash(); + + if (!newBlockHash.equals(getLastBlockSeenHash())) { + // Store the new block hash. + setLastBlockSeenHash(newBlockHash); + + // Notify all the BUILDING transactions of the new block. + // This is so that they can update their work done and depth. + Set transactions = getTransactions(true, false); + + for (Transaction t : transactions) { + t.getConfidence().notifyWorkDone(block.getHeader()); + } + } + } + } + + // Update the transaction confidence and timestamp bookkeeping data. + invokeOnTransactionConfidenceChanged(tx); + // Inform anyone interested that we have received or sent coins but only if: // - This is not due to a re-org. // - The coins appeared on the best chain. @@ -598,7 +609,7 @@ public class Wallet implements Serializable { invokeOnCoinsSent(tx, prevBalance, newBalance); } } - + checkState(isConsistent()); } @@ -789,7 +800,6 @@ public class Wallet implements Serializable { /** * Returns a set of all transactions in the wallet. - * * @param includeDead If true, transactions that were overridden by a double spend are included. * @param includeInactive If true, transactions that are on side chains (are unspendable) are included. */ @@ -1370,7 +1380,7 @@ public class Wallet implements Serializable { * * The oldBlocks/newBlocks lists are ordered height-wise from top first to bottom last. */ - synchronized void reorganize(List oldBlocks, List newBlocks) throws VerificationException { + synchronized void reorganize(StoredBlock splitPoint, List oldBlocks, List newBlocks) throws VerificationException { // This runs on any peer thread with the block chain synchronized. // // The reorganize functionality of the wallet is tested in ChainSplitTests. @@ -1486,9 +1496,32 @@ public class Wallet implements Serializable { // Now replay the act of receiving the blocks that were previously in a side chain. This will: // - Move any transactions that were pending and are now accepted into the right bucket. // - Connect the newly active transactions. + Collections.reverse(newBlocks); // Need bottom-to-top but we get top-to-bottom. + + // The old blocks have contributed to the depth and work done for all the transactions in the + // wallet that are in blocks up to and including the chain split block. + // The total depth and work done is calculated here and then subtracted from the appropriate transactions. + int depthToSubtract = oldBlocks == null ? 0 : oldBlocks.size(); + + BigInteger workDoneToSubtract = BigInteger.ZERO; + for (StoredBlock b : oldBlocks) { + workDoneToSubtract = workDoneToSubtract.add(b.getHeader().getWork()); + } + log.info("DepthToSubtract = " + depthToSubtract + ", workDoneToSubtract = " + workDoneToSubtract); + + // Remove depthToSubtract and workDoneToSubtract from all transactions in the wallet except for pending and inactive + // (i.e. the transactions in the two chains of blocks we are reorganising). + subtractDepthAndWorkDone(depthToSubtract, workDoneToSubtract, spent.values()); + subtractDepthAndWorkDone(depthToSubtract, workDoneToSubtract, unspent.values()); + subtractDepthAndWorkDone(depthToSubtract, workDoneToSubtract, dead.values()); + + // The effective last seen block is now the split point so set the lastSeenBlockHash. + setLastBlockSeenHash(splitPoint.getHeader().getHash()); + for (StoredBlock b : newBlocks) { log.info("Replaying block {}", b.getHeader().getHashAsString()); + Set txns = new HashSet(); Sha256Hash blockHash = b.getHeader().getHash(); for (Transaction tx : newChainTransactions.values()) { @@ -1497,11 +1530,26 @@ public class Wallet implements Serializable { log.info(" containing tx {}", tx.getHashAsString()); } } - for (Transaction t : txns) { - try { - receive(t, b, BlockChain.NewBlockType.BEST_CHAIN, true); - } catch (ScriptException e) { - throw new RuntimeException(e); // Cannot happen as these blocks were already verified. + + if (txns.isEmpty()) { + // If there are no new transactions in this block we still need to bury existing transactions one block deeper. + for (Transaction t : spent.values()) { + t.getConfidence().notifyWorkDone(b.getHeader()); + } + for (Transaction t : unspent.values()) { + t.getConfidence().notifyWorkDone(b.getHeader()); + } + for (Transaction t : dead.values()) { + t.getConfidence().notifyWorkDone(b.getHeader()); + } + } else { + // Add the transactions to the new blocks. + for (Transaction t : txns) { + try { + receive(t, b, BlockChain.NewBlockType.BEST_CHAIN, true); + } catch (ScriptException e) { + throw new RuntimeException(e); // Cannot happen as these blocks were already verified. + } } } } @@ -1550,6 +1598,18 @@ public class Wallet implements Serializable { checkState(isConsistent()); } + /** + * Subtract the supplied depth and work done from the given transactions. + */ + synchronized private void subtractDepthAndWorkDone(int depthToSubtract, BigInteger workDoneToSubtract, Collection transactions) { + for (Transaction tx : transactions) { + if (tx.getConfidence().getConfidenceType() == ConfidenceType.BUILDING) { + tx.getConfidence().setDepthInBlocks(tx.getConfidence().getDepthInBlocks() - depthToSubtract); + tx.getConfidence().setWorkDone(tx.getConfidence().getWorkDone().subtract(workDoneToSubtract)); + } + } + } + private void reprocessUnincludedTxAfterReorg(Map pool, Transaction tx) { log.info("TX {}", tx.getHashAsString()); int numInputs = tx.getInputs().size(); diff --git a/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java b/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java index 0899c234..c7adea26 100644 --- a/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java +++ b/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java @@ -201,6 +201,10 @@ public class WalletProtobufSerializer { confidenceBuilder.setType(Protos.TransactionConfidence.Type.valueOf(confidence.getConfidenceType().getValue())); if (confidence.getConfidenceType() == ConfidenceType.BUILDING) { confidenceBuilder.setAppearedAtHeight(confidence.getAppearedAtChainHeight()); + confidenceBuilder.setDepth(confidence.getDepthInBlocks()); + if (confidence.getWorkDone() != null) { + confidenceBuilder.setWorkDone(confidence.getWorkDone().longValue()); + } } if (confidence.getConfidenceType() == ConfidenceType.OVERRIDDEN_BY_DOUBLE_SPEND) { Sha256Hash overridingHash = confidence.getOverridingTransaction().getHash(); @@ -361,6 +365,20 @@ public class WalletProtobufSerializer { } confidence.setAppearedAtChainHeight(confidenceProto.getAppearedAtHeight()); } + if (confidenceProto.hasDepth()) { + if (confidence.getConfidenceType() != ConfidenceType.BUILDING) { + log.warn("Have depth but not BUILDING for tx {}", tx.getHashAsString()); + return; + } + confidence.setDepthInBlocks(confidenceProto.getDepth()); + } + if (confidenceProto.hasWorkDone()) { + if (confidence.getConfidenceType() != ConfidenceType.BUILDING) { + log.warn("Have workDone but not BUILDING for tx {}", tx.getHashAsString()); + return; + } + confidence.setWorkDone(BigInteger.valueOf(confidenceProto.getWorkDone())); + } if (confidenceProto.hasOverridingTransaction()) { if (confidence.getConfidenceType() != ConfidenceType.OVERRIDDEN_BY_DOUBLE_SPEND) { log.warn("Have overridingTransaction but not OVERRIDDEN for tx {}", tx.getHashAsString()); diff --git a/core/src/main/java/org/bitcoinj/wallet/Protos.java b/core/src/main/java/org/bitcoinj/wallet/Protos.java index be1dac1b..3818826f 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Protos.java +++ b/core/src/main/java/org/bitcoinj/wallet/Protos.java @@ -1784,6 +1784,14 @@ public final class Protos { // optional bytes overriding_transaction = 3; boolean hasOverridingTransaction(); com.google.protobuf.ByteString getOverridingTransaction(); + + // optional int32 depth = 4; + boolean hasDepth(); + int getDepth(); + + // optional int64 work_done = 5; + boolean hasWorkDone(); + long getWorkDone(); } public static final class TransactionConfidence extends com.google.protobuf.GeneratedMessage @@ -1922,10 +1930,32 @@ public final class Protos { return overridingTransaction_; } + // optional int32 depth = 4; + public static final int DEPTH_FIELD_NUMBER = 4; + private int depth_; + public boolean hasDepth() { + return ((bitField0_ & 0x00000008) == 0x00000008); + } + public int getDepth() { + return depth_; + } + + // optional int64 work_done = 5; + public static final int WORK_DONE_FIELD_NUMBER = 5; + private long workDone_; + public boolean hasWorkDone() { + return ((bitField0_ & 0x00000010) == 0x00000010); + } + public long getWorkDone() { + return workDone_; + } + private void initFields() { type_ = org.bitcoinj.wallet.Protos.TransactionConfidence.Type.UNKNOWN; appearedAtHeight_ = 0; overridingTransaction_ = com.google.protobuf.ByteString.EMPTY; + depth_ = 0; + workDone_ = 0L; } private byte memoizedIsInitialized = -1; public final boolean isInitialized() { @@ -1948,6 +1978,12 @@ public final class Protos { if (((bitField0_ & 0x00000004) == 0x00000004)) { output.writeBytes(3, overridingTransaction_); } + if (((bitField0_ & 0x00000008) == 0x00000008)) { + output.writeInt32(4, depth_); + } + if (((bitField0_ & 0x00000010) == 0x00000010)) { + output.writeInt64(5, workDone_); + } getUnknownFields().writeTo(output); } @@ -1969,6 +2005,14 @@ public final class Protos { size += com.google.protobuf.CodedOutputStream .computeBytesSize(3, overridingTransaction_); } + if (((bitField0_ & 0x00000008) == 0x00000008)) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(4, depth_); + } + if (((bitField0_ & 0x00000010) == 0x00000010)) { + size += com.google.protobuf.CodedOutputStream + .computeInt64Size(5, workDone_); + } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; return size; @@ -2099,6 +2143,10 @@ public final class Protos { bitField0_ = (bitField0_ & ~0x00000002); overridingTransaction_ = com.google.protobuf.ByteString.EMPTY; bitField0_ = (bitField0_ & ~0x00000004); + depth_ = 0; + bitField0_ = (bitField0_ & ~0x00000008); + workDone_ = 0L; + bitField0_ = (bitField0_ & ~0x00000010); return this; } @@ -2149,6 +2197,14 @@ public final class Protos { to_bitField0_ |= 0x00000004; } result.overridingTransaction_ = overridingTransaction_; + if (((from_bitField0_ & 0x00000008) == 0x00000008)) { + to_bitField0_ |= 0x00000008; + } + result.depth_ = depth_; + if (((from_bitField0_ & 0x00000010) == 0x00000010)) { + to_bitField0_ |= 0x00000010; + } + result.workDone_ = workDone_; result.bitField0_ = to_bitField0_; onBuilt(); return result; @@ -2174,6 +2230,12 @@ public final class Protos { if (other.hasOverridingTransaction()) { setOverridingTransaction(other.getOverridingTransaction()); } + if (other.hasDepth()) { + setDepth(other.getDepth()); + } + if (other.hasWorkDone()) { + setWorkDone(other.getWorkDone()); + } this.mergeUnknownFields(other.getUnknownFields()); return this; } @@ -2226,6 +2288,16 @@ public final class Protos { overridingTransaction_ = input.readBytes(); break; } + case 32: { + bitField0_ |= 0x00000008; + depth_ = input.readInt32(); + break; + } + case 40: { + bitField0_ |= 0x00000010; + workDone_ = input.readInt64(); + break; + } } } } @@ -2301,6 +2373,48 @@ public final class Protos { return this; } + // optional int32 depth = 4; + private int depth_ ; + public boolean hasDepth() { + return ((bitField0_ & 0x00000008) == 0x00000008); + } + public int getDepth() { + return depth_; + } + public Builder setDepth(int value) { + bitField0_ |= 0x00000008; + depth_ = value; + onChanged(); + return this; + } + public Builder clearDepth() { + bitField0_ = (bitField0_ & ~0x00000008); + depth_ = 0; + onChanged(); + return this; + } + + // optional int64 work_done = 5; + private long workDone_ ; + public boolean hasWorkDone() { + return ((bitField0_ & 0x00000010) == 0x00000010); + } + public long getWorkDone() { + return workDone_; + } + public Builder setWorkDone(long value) { + bitField0_ |= 0x00000010; + workDone_ = value; + onChanged(); + return this; + } + public Builder clearWorkDone() { + bitField0_ = (bitField0_ & ~0x00000010); + workDone_ = 0L; + onChanged(); + return this; + } + // @@protoc_insertion_point(builder_scope:wallet.TransactionConfidence) } @@ -5673,30 +5787,31 @@ public final class Protos { "\003 \002(\014\022\020\n\010sequence\030\004 \001(\r\"\177\n\021TransactionOu" + "tput\022\r\n\005value\030\001 \002(\003\022\024\n\014script_bytes\030\002 \002(" + "\014\022!\n\031spent_by_transaction_hash\030\003 \001(\014\022\"\n\032", - "spent_by_transaction_index\030\004 \001(\005\"\366\001\n\025Tra" + + "spent_by_transaction_index\030\004 \001(\005\"\230\002\n\025Tra" + "nsactionConfidence\0220\n\004type\030\001 \001(\0162\".walle" + "t.TransactionConfidence.Type\022\032\n\022appeared" + "_at_height\030\002 \001(\005\022\036\n\026overriding_transacti" + - "on\030\003 \001(\014\"o\n\004Type\022\013\n\007UNKNOWN\020\000\022\014\n\010BUILDIN" + - "G\020\001\022\025\n\021NOT_SEEN_IN_CHAIN\020\002\022\025\n\021NOT_IN_BES" + - "T_CHAIN\020\003\022\036\n\032OVERRIDDEN_BY_DOUBLE_SPEND\020" + - "\004\"\211\003\n\013Transaction\022\017\n\007version\030\001 \002(\005\022\014\n\004ha" + - "sh\030\002 \002(\014\022&\n\004pool\030\003 \001(\0162\030.wallet.Transact" + - "ion.Pool\022\021\n\tlock_time\030\004 \001(\r\022\022\n\nupdated_a", - "t\030\005 \001(\003\0223\n\021transaction_input\030\006 \003(\0132\030.wal" + - "let.TransactionInput\0225\n\022transaction_outp" + - "ut\030\007 \003(\0132\031.wallet.TransactionOutput\022\022\n\nb" + - "lock_hash\030\010 \003(\014\0221\n\nconfidence\030\t \001(\0132\035.wa" + - "llet.TransactionConfidence\"Y\n\004Pool\022\013\n\007UN" + - "SPENT\020\004\022\t\n\005SPENT\020\005\022\014\n\010INACTIVE\020\002\022\010\n\004DEAD" + - "\020\n\022\013\n\007PENDING\020\020\022\024\n\020PENDING_INACTIVE\020\022\"8\n" + - "\tExtension\022\n\n\002id\030\001 \002(\t\022\014\n\004data\030\002 \002(\014\022\021\n\t" + - "mandatory\030\003 \002(\010\"\254\001\n\006Wallet\022\032\n\022network_id" + - "entifier\030\001 \002(\t\022\034\n\024last_seen_block_hash\030\002", - " \001(\014\022\030\n\003key\030\003 \003(\0132\013.wallet.Key\022(\n\013transa" + - "ction\030\004 \003(\0132\023.wallet.Transaction\022$\n\texte" + - "nsion\030\n \003(\0132\021.wallet.ExtensionB\035\n\023org.bi" + - "tcoinj.walletB\006Protos" + "on\030\003 \001(\014\022\r\n\005depth\030\004 \001(\005\022\021\n\twork_done\030\005 \001" + + "(\003\"o\n\004Type\022\013\n\007UNKNOWN\020\000\022\014\n\010BUILDING\020\001\022\025\n" + + "\021NOT_SEEN_IN_CHAIN\020\002\022\025\n\021NOT_IN_BEST_CHAI" + + "N\020\003\022\036\n\032OVERRIDDEN_BY_DOUBLE_SPEND\020\004\"\211\003\n\013" + + "Transaction\022\017\n\007version\030\001 \002(\005\022\014\n\004hash\030\002 \002" + + "(\014\022&\n\004pool\030\003 \001(\0162\030.wallet.Transaction.Po", + "ol\022\021\n\tlock_time\030\004 \001(\r\022\022\n\nupdated_at\030\005 \001(" + + "\003\0223\n\021transaction_input\030\006 \003(\0132\030.wallet.Tr" + + "ansactionInput\0225\n\022transaction_output\030\007 \003" + + "(\0132\031.wallet.TransactionOutput\022\022\n\nblock_h" + + "ash\030\010 \003(\014\0221\n\nconfidence\030\t \001(\0132\035.wallet.T" + + "ransactionConfidence\"Y\n\004Pool\022\013\n\007UNSPENT\020" + + "\004\022\t\n\005SPENT\020\005\022\014\n\010INACTIVE\020\002\022\010\n\004DEAD\020\n\022\013\n\007" + + "PENDING\020\020\022\024\n\020PENDING_INACTIVE\020\022\"8\n\tExten" + + "sion\022\n\n\002id\030\001 \002(\t\022\014\n\004data\030\002 \002(\014\022\021\n\tmandat" + + "ory\030\003 \002(\010\"\254\001\n\006Wallet\022\032\n\022network_identifi", + "er\030\001 \002(\t\022\034\n\024last_seen_block_hash\030\002 \001(\014\022\030" + + "\n\003key\030\003 \003(\0132\013.wallet.Key\022(\n\013transaction\030" + + "\004 \003(\0132\023.wallet.Transaction\022$\n\textension\030" + + "\n \003(\0132\021.wallet.ExtensionB\035\n\023org.bitcoinj" + + ".walletB\006Protos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { @@ -5732,7 +5847,7 @@ public final class Protos { internal_static_wallet_TransactionConfidence_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_wallet_TransactionConfidence_descriptor, - new java.lang.String[] { "Type", "AppearedAtHeight", "OverridingTransaction", }, + new java.lang.String[] { "Type", "AppearedAtHeight", "OverridingTransaction", "Depth", "WorkDone", }, org.bitcoinj.wallet.Protos.TransactionConfidence.class, org.bitcoinj.wallet.Protos.TransactionConfidence.Builder.class); internal_static_wallet_Transaction_descriptor = diff --git a/core/src/test/java/com/google/bitcoin/core/ChainSplitTests.java b/core/src/test/java/com/google/bitcoin/core/ChainSplitTest.java similarity index 79% rename from core/src/test/java/com/google/bitcoin/core/ChainSplitTests.java rename to core/src/test/java/com/google/bitcoin/core/ChainSplitTest.java index caad8f80..0409bce3 100644 --- a/core/src/test/java/com/google/bitcoin/core/ChainSplitTests.java +++ b/core/src/test/java/com/google/bitcoin/core/ChainSplitTest.java @@ -26,11 +26,12 @@ import java.util.ArrayList; import static org.junit.Assert.*; -public class ChainSplitTests { +public class ChainSplitTest { private NetworkParameters unitTestParams; private Wallet wallet; private BlockChain chain; private Address coinsTo; + private Address coinsTo2; private Address someOtherGuy; @Before @@ -39,8 +40,10 @@ public class ChainSplitTests { unitTestParams = NetworkParameters.unitTests(); wallet = new Wallet(unitTestParams); wallet.addKey(new ECKey()); + wallet.addKey(new ECKey()); chain = new BlockChain(unitTestParams, wallet, new MemoryBlockStore(unitTestParams)); coinsTo = wallet.keychain.get(0).toAddress(unitTestParams); + coinsTo2 = wallet.keychain.get(1).toAddress(unitTestParams); someOtherGuy = new ECKey().toAddress(unitTestParams); } @@ -283,7 +286,7 @@ public class ChainSplitTests { public void txConfidenceLevels() throws Exception { // Check that as the chain forks and re-orgs, the confidence data associated with each transaction is // maintained correctly. - final ArrayList txns = new ArrayList(2); + final ArrayList txns = new ArrayList(3); wallet.addEventListener(new AbstractWalletEventListener() { @Override public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { @@ -291,62 +294,125 @@ public class ChainSplitTests { } }); - // Start by building a couple of blocks on top of the genesis block. + // Start by building three blocks on top of the genesis block. Block b1 = unitTestParams.genesisBlock.createNextBlock(coinsTo); - Block b2 = b1.createNextBlock(coinsTo); + BigInteger work1 = b1.getWork(); + + Block b2 = b1.createNextBlock(coinsTo2); + BigInteger work2 = b2.getWork(); + + Block b3 = b2.createNextBlock(coinsTo2); + BigInteger work3 = b3.getWork(); + assertTrue(chain.add(b1)); assertTrue(chain.add(b2)); + assertTrue(chain.add(b3)); + // Check the transaction confidence levels are correct. - assertEquals(2, txns.size()); + assertEquals(3, txns.size()); + assertEquals(1, txns.get(0).getConfidence().getAppearedAtChainHeight()); assertEquals(2, txns.get(1).getConfidence().getAppearedAtChainHeight()); - assertEquals(1, txns.get(1).getConfidence().getDepthInBlocks(chain)); - assertEquals(2, txns.get(0).getConfidence().getDepthInBlocks(chain)); - assertEquals(10, txns.get(0).getConfidence().getWorkDone(chain).intValue()); - assertEquals(6, txns.get(1).getConfidence().getWorkDone(chain).intValue()); + assertEquals(3, txns.get(2).getConfidence().getAppearedAtChainHeight()); + + assertEquals(3, txns.get(0).getConfidence().getDepthInBlocks()); + assertEquals(2, txns.get(1).getConfidence().getDepthInBlocks()); + assertEquals(1, txns.get(2).getConfidence().getDepthInBlocks()); + + assertEquals(work1.add(work2).add(work3), txns.get(0).getConfidence().getWorkDone()); + assertEquals(work2.add(work3), txns.get(1).getConfidence().getWorkDone()); + assertEquals(work3, txns.get(2).getConfidence().getWorkDone()); + // We now have the following chain: - // genesis -> b1 -> b2 + // genesis -> b1 -> b2 -> b3 // // so fork like this: // - // genesis -> b1 -> b2 - // \-> b3 + // genesis -> b1 -> b2 -> b3 + // \-> b4 -> b5 // - // Nothing should happen at this point. We saw b2 first so it takes priority. - Block b3 = b1.createNextBlock(someOtherGuy); - assertTrue(chain.add(b3)); - assertEquals(2, txns.size()); + // Nothing should happen at this point. We saw b2 and b3 first so it takes priority. + Block b4 = b1.createNextBlock(someOtherGuy); + BigInteger work4 = b4.getWork(); + + Block b5 = b4.createNextBlock(someOtherGuy); + BigInteger work5 = b5.getWork(); + + assertTrue(chain.add(b4)); + assertTrue(chain.add(b5)); + assertEquals(3, txns.size()); + assertEquals(1, txns.get(0).getConfidence().getAppearedAtChainHeight()); assertEquals(2, txns.get(1).getConfidence().getAppearedAtChainHeight()); + assertEquals(3, txns.get(2).getConfidence().getAppearedAtChainHeight()); + + assertEquals(3, txns.get(0).getConfidence().getDepthInBlocks()); + assertEquals(2, txns.get(1).getConfidence().getDepthInBlocks()); + assertEquals(1, txns.get(2).getConfidence().getDepthInBlocks()); + + assertEquals(work1.add(work2).add(work3), txns.get(0).getConfidence().getWorkDone()); + assertEquals(work2.add(work3), txns.get(1).getConfidence().getWorkDone()); + assertEquals(work3, txns.get(2).getConfidence().getWorkDone()); + // Now we add another block to make the alternative chain longer. - assertTrue(chain.add(b3.createNextBlock(someOtherGuy))); + Block b6 = b5.createNextBlock(someOtherGuy); + BigInteger work6 = b6.getWork(); + assertTrue(chain.add(b6)); // - // genesis -> b1 -> b2 - // \-> b3 -> b4 + // genesis -> b1 -> b2 -> b3 + // \-> b4 -> b5 -> b6 // - assertEquals(2, txns.size()); + + assertEquals(3, txns.size()); assertEquals(1, txns.get(0).getConfidence().getAppearedAtChainHeight()); + assertEquals(4, txns.get(0).getConfidence().getDepthInBlocks()); + assertEquals(work1.add(work4).add(work5).add(work6), txns.get(0).getConfidence().getWorkDone()); + + // Transaction 1 (in block b2) is now on a side chain. assertEquals(TransactionConfidence.ConfidenceType.NOT_IN_BEST_CHAIN, txns.get(1).getConfidence().getConfidenceType()); try { txns.get(1).getConfidence().getAppearedAtChainHeight(); fail(); } catch (IllegalStateException e) {} + try { + txns.get(1).getConfidence().getDepthInBlocks(); + fail(); + } catch (IllegalStateException e) {} + try { + txns.get(1).getConfidence().getWorkDone(); + fail(); + } catch (IllegalStateException e) {} // ... and back to the first chain. - Block b5 = b2.createNextBlock(coinsTo); - Block b6 = b5.createNextBlock(coinsTo); - assertTrue(chain.add(b5)); - assertTrue(chain.add(b6)); + Block b7 = b3.createNextBlock(coinsTo); + BigInteger work7 = b7.getWork(); + + Block b8 = b7.createNextBlock(coinsTo); + BigInteger work8 = b7.getWork(); + + assertTrue(chain.add(b7)); + + assertTrue(chain.add(b8)); // - // genesis -> b1 -> b2 -> b5 -> b6 - // \-> b3 -> b4 + // genesis -> b1 -> b2 -> b3 -> b7 -> b8 + // \-> b4 -> b5 -> b6 // // This should be enabled, once we figure out the best way to inform the user of how the wallet is changing // during the re-org. - // assertEquals(4, txns.size()); + //assertEquals(5, txns.size()); assertEquals(1, txns.get(0).getConfidence().getAppearedAtChainHeight()); assertEquals(2, txns.get(1).getConfidence().getAppearedAtChainHeight()); - assertEquals("200.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); + assertEquals(3, txns.get(2).getConfidence().getAppearedAtChainHeight()); + + assertEquals(5, txns.get(0).getConfidence().getDepthInBlocks()); + assertEquals(4, txns.get(1).getConfidence().getDepthInBlocks()); + assertEquals(3, txns.get(2).getConfidence().getDepthInBlocks()); + + assertEquals(work1.add(work2).add(work3).add(work7).add(work8), txns.get(0).getConfidence().getWorkDone()); + assertEquals(work2.add(work3).add(work7).add(work8), txns.get(1).getConfidence().getWorkDone()); + assertEquals(work3.add(work7).add(work8), txns.get(2).getConfidence().getWorkDone()); + + assertEquals("250.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); } } diff --git a/core/src/test/java/com/google/bitcoin/core/WalletTest.java b/core/src/test/java/com/google/bitcoin/core/WalletTest.java index 5c2b6089..75e8f804 100644 --- a/core/src/test/java/com/google/bitcoin/core/WalletTest.java +++ b/core/src/test/java/com/google/bitcoin/core/WalletTest.java @@ -653,7 +653,6 @@ public class WalletTest { // Now check we can serialize old wallets to protocol buffers. Covers bug 134. bios.reset(); new WalletProtobufSerializer().writeWallet(wallet, bios); - } @Test diff --git a/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java b/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java index 135bc4c3..afdb4492 100644 --- a/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java +++ b/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java @@ -17,6 +17,9 @@ import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Random; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Set; import static com.google.bitcoin.core.TestUtils.createFakeTx; import static org.junit.Assert.*; @@ -128,7 +131,7 @@ public class WalletProtobufSerializerTest { Sha256Hash blockHash = block.getHash(); wallet.setLastBlockSeenHash(blockHash); - // Roundtrip the wallet and check it has stored te blockHash. + // Roundtrip the wallet and check it has stored the blockHash. Wallet wallet1 = roundTrip(wallet); assertEquals(blockHash, wallet1.getLastBlockSeenHash()); @@ -139,6 +142,83 @@ public class WalletProtobufSerializerTest { assertEquals(genesisBlock.getHash(), wallet2.getLastBlockSeenHash()); } + @Test + public void testAppearedAtChainHeightDepthAndWorkDone() throws Exception { + // Test the TransactionConfidence appearedAtChainHeight, depth and workDone field are stored. + + BlockChain chain = new BlockChain(params, myWallet, new MemoryBlockStore(params)); + + final ArrayList txns = new ArrayList(2); + myWallet.addEventListener(new AbstractWalletEventListener() { + @Override + public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { + txns.add(tx); + } + }); + + // Start by building two blocks on top of the genesis block. + Block b1 = params.genesisBlock.createNextBlock(myAddress); + BigInteger work1 = b1.getWork(); + assertTrue(work1.compareTo(BigInteger.ZERO) > 0); + + Block b2 = b1.createNextBlock(myAddress); + BigInteger work2 = b2.getWork(); + assertTrue(work2.compareTo(BigInteger.ZERO) > 0); + + assertTrue(chain.add(b1)); + assertTrue(chain.add(b2)); + + // We now have the following chain: + // genesis -> b1 -> b2 + + // Check the transaction confidence levels are correct before wallet roundtrip. + assertEquals(2, txns.size()); + + TransactionConfidence confidence0 = txns.get(0).getConfidence(); + TransactionConfidence confidence1 = txns.get(1).getConfidence(); + + assertEquals(1, confidence0.getAppearedAtChainHeight()); + assertEquals(2, confidence1.getAppearedAtChainHeight()); + + assertEquals(2, confidence0.getDepthInBlocks()); + assertEquals(1, confidence1.getDepthInBlocks()); + + assertEquals(work1.add(work2), confidence0.getWorkDone()); + assertEquals(work2, confidence1.getWorkDone()); + + // Roundtrip the wallet and check it has stored the depth and workDone. + Wallet rebornWallet = roundTrip(myWallet); + + Set rebornTxns = rebornWallet.getTransactions(false, false); + assertEquals(2, rebornTxns.size()); + + // The transactions are not guaranteed to be in the same order so sort them to be in chain height order if required. + Iterator it = rebornTxns.iterator(); + Transaction txA = it.next(); + Transaction txB = it.next(); + + Transaction rebornTx0, rebornTx1; + if (txA.getConfidence().getAppearedAtChainHeight() == 1) { + rebornTx0 = txA; + rebornTx1 = txB; + } else { + rebornTx0 = txB; + rebornTx1 = txA; + } + + TransactionConfidence rebornConfidence0 = rebornTx0.getConfidence(); + TransactionConfidence rebornConfidence1 = rebornTx1.getConfidence(); + + assertEquals(1, rebornConfidence0.getAppearedAtChainHeight()); + assertEquals(2, rebornConfidence1.getAppearedAtChainHeight()); + + assertEquals(2, rebornConfidence0.getDepthInBlocks()); + assertEquals(1, rebornConfidence1.getDepthInBlocks()); + + assertEquals(work1.add(work2), rebornConfidence0.getWorkDone()); + assertEquals(work2, rebornConfidence1.getWorkDone()); + } + private Wallet roundTrip(Wallet wallet) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); //System.out.println(WalletProtobufSerializer.walletToText(wallet)); diff --git a/examples/src/main/java/com/google/bitcoin/examples/PingService.java b/examples/src/main/java/com/google/bitcoin/examples/PingService.java index eea45eeb..b0afe99d 100644 --- a/examples/src/main/java/com/google/bitcoin/examples/PingService.java +++ b/examples/src/main/java/com/google/bitcoin/examples/PingService.java @@ -125,13 +125,9 @@ public class PingService { System.out.println("Confidences of wallet transactions:"); for (Transaction tx : transactions) { System.out.println(tx); - try { - System.out.println(tx.getConfidence()); - if (tx.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) - System.out.println("Work done: " + tx.getConfidence().getWorkDone(chain).toString()); - } catch (BlockStoreException e) { - throw new RuntimeException(e); - } + System.out.println(tx.getConfidence()); + if (tx.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) + System.out.println("Work done: " + tx.getConfidence().getWorkDone().toString()); System.out.println(); } }