3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-01-31 15:22:16 +00:00
This commit is contained in:
Mike Hearn 2012-01-21 17:56:53 +01:00
commit 3eb12c8e97
6 changed files with 107 additions and 85 deletions

View File

@ -254,6 +254,7 @@
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<!--
<plugin> <plugin>
<groupId>org.codehaus.mojo</groupId> <groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId> <artifactId>build-helper-maven-plugin</artifactId>
@ -273,6 +274,7 @@
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
-->
</plugins> </plugins>
</build> </build>

View File

@ -22,7 +22,7 @@ import org.slf4j.LoggerFactory;
import java.io.*; import java.io.*;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.*; import java.util.*;
import static com.google.bitcoin.core.Utils.*; import static com.google.bitcoin.core.Utils.*;
/** /**
@ -56,15 +56,9 @@ public class Transaction extends ChildMessage implements Serializable {
private long lockTime; private long lockTime;
// This is only stored in Java serialization. It records which blocks (and their height + work) the transaction // This is being migrated to appearsInHashes
// has been included in. For most transactions this set will have a single member. In the case of a chain split a
// transaction may appear in multiple blocks but only one of them is part of the best chain. It's not valid to
// have an identical transaction appear in two blocks in the same chain but this invariant is expensive to check,
// so it's not directly enforced anywhere.
//
// If this transaction is not stored in the wallet, appearsIn is null.
Set<StoredBlock> appearsIn; Set<StoredBlock> appearsIn;
// Stored only in Java serialization. This is either the time the transaction was broadcast as measured from the // Stored only in Java serialization. This is either the time the transaction was broadcast as measured from the
// local clock, or the time from the block in which it was included. Note that this can be changed by re-orgs so // local clock, or the time from the block in which it was included. Note that this can be changed by re-orgs so
// the wallet may update this field. Old serialized transactions don't have this field, thus null is valid. // the wallet may update this field. Old serialized transactions don't have this field, thus null is valid.
@ -78,6 +72,15 @@ public class Transaction extends ChildMessage implements Serializable {
// Data about how confirmed this tx is. Serialized, may be null. // Data about how confirmed this tx is. Serialized, may be null.
private TransactionConfidence confidence; private TransactionConfidence confidence;
// This records which blocks the transaction
// has been included in. For most transactions this set will have a single member. In the case of a chain split a
// transaction may appear in multiple blocks but only one of them is part of the best chain. It's not valid to
// have an identical transaction appear in two blocks in the same chain but this invariant is expensive to check,
// so it's not directly enforced anywhere.
//
// If this transaction is not stored in the wallet, appearsInHashes is null.
Set<Sha256Hash> appearsInHashes;
public Transaction(NetworkParameters params) { public Transaction(NetworkParameters params) {
super(params); super(params);
version = 1; version = 1;
@ -190,8 +193,21 @@ public class Transaction extends ChildMessage implements Serializable {
* Returns a set of blocks which contain the transaction, or null if this transaction doesn't have that data * Returns a set of blocks which contain the transaction, or null if this transaction doesn't have that data
* because it's not stored in the wallet or because it has never appeared in a block. * because it's not stored in the wallet or because it has never appeared in a block.
*/ */
public Set<StoredBlock> getAppearsIn() { public Collection<Sha256Hash> getAppearsInHashes() {
return appearsIn; if (appearsInHashes != null)
return appearsInHashes;
if (appearsIn != null) {
assert appearsInHashes == null;
log.info("Migrating a tx to appearsInHashes");
appearsInHashes = new HashSet<Sha256Hash>(appearsIn.size());
for (StoredBlock block : appearsIn) {
appearsInHashes.add(block.getHeader().getHash());
}
appearsIn = null;
}
return appearsInHashes;
} }
/** /**
@ -207,31 +223,36 @@ public class Transaction extends ChildMessage implements Serializable {
* used by the wallet to ensure transactions that appear on side chains are recorded properly even though the * used by the wallet to ensure transactions that appear on side chains are recorded properly even though the
* block stores do not save the transaction data at all.<p> * block stores do not save the transaction data at all.<p>
* *
* If there is a re-org this will be called once for each block that was previously seen, to update which block * <p>If there is a re-org this will be called once for each block that was previously seen, to update which block
* is the best chain. The best chain block is guaranteed to be called last. So this must be idempotent. * is the best chain. The best chain block is guaranteed to be called last. So this must be idempotent.
* *
* <p>Sets updatedAt to be the earliest valid block time where this tx was seen
*
* @param block The {@link StoredBlock} in which the transaction has appeared. * @param block The {@link StoredBlock} in which the transaction has appeared.
* @param bestChain whether to set the updatedAt timestamp from the block header (only if not already set) * @param bestChain whether to set the updatedAt timestamp from the block header (only if not already set)
*/ */
public void setBlockAppearance(StoredBlock block, boolean bestChain) { public void setBlockAppearance(StoredBlock block, boolean bestChain) {
if (bestChain && updatedAt == null) { long blockTime = block.getHeader().getTimeSeconds() * 1000;
updatedAt = new Date(block.getHeader().getTimeSeconds() * 1000); if (bestChain && (updatedAt == null || updatedAt.getTime() == 0 || updatedAt.getTime() > blockTime)) {
updatedAt = new Date(blockTime);
} }
if (appearsIn == null) {
appearsIn = new HashSet<StoredBlock>(); addBlockAppearance(block.getHeader().getHash());
}
appearsIn.add(block);
if (bestChain) { if (bestChain) {
if (updatedAt == null) {
updatedAt = new Date(block.getHeader().getTimeSeconds() * 1000);
}
// 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 this line completes, the wallets
// state may have changed! // state may have changed!
getConfidence().setAppearedAtChainHeight(block.getHeight()); getConfidence().setAppearedAtChainHeight(block.getHeight());
} }
} }
public void addBlockAppearance(final Sha256Hash blockHash) {
if (appearsInHashes == null) {
appearsInHashes = new HashSet<Sha256Hash>();
}
appearsInHashes.add(blockHash);
}
/** Called by the wallet once a re-org means we don't appear in the best chain anymore. */ /** Called by the wallet once a re-org means we don't appear in the best chain anymore. */
void notifyNotOnBestChain() { void notifyNotOnBestChain() {
getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.NOT_IN_BEST_CHAIN); getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.NOT_IN_BEST_CHAIN);
@ -324,26 +345,12 @@ public class Transaction extends ChildMessage implements Serializable {
/** /**
* Returns the earliest time at which the transaction was seen (broadcast or included into the chain), * Returns the earliest time at which the transaction was seen (broadcast or included into the chain),
* or null if that information isn't available. * or the epoch if that information isn't available.
*/ */
public Date getUpdateTime() { public Date getUpdateTime() {
if (updatedAt == null) { if (updatedAt == null) {
// Older wallets did not store this field. If we can, fill it out based on the block pointers. We might // Older wallets did not store this field. Set to the epoch.
// "guess wrong" in the case of transactions appearing on chain forks, but this is unlikely to matter in updatedAt = new Date(0);
// practice. Note, some patched copies of BitCoinJ store dates in this field that do not correspond to any
// block but rather broadcast time.
if (appearsIn == null || appearsIn.size() == 0) {
// Transaction came from somewhere that doesn't provide time info.
return null;
}
long earliestTimeSecs = Long.MAX_VALUE;
// We might return a time that is different to the best chain, as we don't know here which block is part
// of the active chain and which are simply inactive. We just ignore this for now.
// TODO: At some point we'll want to store storing full block headers in the wallet. Remove at that time.
for (StoredBlock b : appearsIn) {
earliestTimeSecs = Math.min(b.getHeader().getTimeSeconds(), earliestTimeSecs);
}
updatedAt = new Date(earliestTimeSecs * 1000);
} }
return updatedAt; return updatedAt;
} }

View File

@ -1158,10 +1158,18 @@ public class Wallet implements Serializable {
// //
// receive() has been called on the block that is triggering the re-org before this is called. // receive() has been called on the block that is triggering the re-org before this is called.
List<Sha256Hash> oldBlockHashes = new ArrayList<Sha256Hash>(oldBlocks.size());
List<Sha256Hash> newBlockHashes = new ArrayList<Sha256Hash>(newBlocks.size());
log.info("Old part of chain (top to bottom):"); log.info("Old part of chain (top to bottom):");
for (StoredBlock b : oldBlocks) log.info(" {}", b.getHeader().getHashAsString()); for (StoredBlock b : oldBlocks) {
log.info(" {}", b.getHeader().getHashAsString());
oldBlockHashes.add(b.getHeader().getHash());
}
log.info("New part of chain (top to bottom):"); log.info("New part of chain (top to bottom):");
for (StoredBlock b : newBlocks) log.info(" {}", b.getHeader().getHashAsString()); for (StoredBlock b : newBlocks) {
log.info(" {}", b.getHeader().getHashAsString());
newBlockHashes.add(b.getHeader().getHash());
}
// Transactions that appear in the old chain segment. // Transactions that appear in the old chain segment.
Map<Sha256Hash, Transaction> oldChainTransactions = new HashMap<Sha256Hash, Transaction>(); Map<Sha256Hash, Transaction> oldChainTransactions = new HashMap<Sha256Hash, Transaction>();
@ -1177,12 +1185,12 @@ public class Wallet implements Serializable {
all.putAll(spent); all.putAll(spent);
all.putAll(inactive); all.putAll(inactive);
for (Transaction tx : all.values()) { for (Transaction tx : all.values()) {
Set<StoredBlock> appearsIn = tx.getAppearsIn(); Collection<Sha256Hash> appearsIn = tx.getAppearsInHashes();
assert appearsIn != null; assert appearsIn != null;
// If the set of blocks this transaction appears in is disjoint with one of the chain segments it means // If the set of blocks this transaction appears in is disjoint with one of the chain segments it means
// the transaction was never incorporated by a miner into that side of the chain. // the transaction was never incorporated by a miner into that side of the chain.
boolean inOldSection = !Collections.disjoint(appearsIn, oldBlocks); boolean inOldSection = !Collections.disjoint(appearsIn, oldBlockHashes);
boolean inNewSection = !Collections.disjoint(appearsIn, newBlocks); boolean inNewSection = !Collections.disjoint(appearsIn, newBlockHashes);
boolean inCommonSection = !inNewSection && !inOldSection; boolean inCommonSection = !inNewSection && !inOldSection;
if (inCommonSection) { if (inCommonSection) {

View File

@ -16,21 +16,9 @@
package com.google.bitcoin.store; package com.google.bitcoin.store;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.bitcoinj.wallet.Protos;
import com.google.bitcoin.core.AddressFormatException;
import com.google.bitcoin.core.ECKey; import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.NetworkParameters; import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.Sha256Hash; import com.google.bitcoin.core.Sha256Hash;
import com.google.bitcoin.core.StoredBlock;
import com.google.bitcoin.core.Transaction; import com.google.bitcoin.core.Transaction;
import com.google.bitcoin.core.TransactionInput; import com.google.bitcoin.core.TransactionInput;
import com.google.bitcoin.core.TransactionOutPoint; import com.google.bitcoin.core.TransactionOutPoint;
@ -40,6 +28,16 @@ import com.google.bitcoin.core.WalletTransaction;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import com.google.protobuf.TextFormat; import com.google.protobuf.TextFormat;
import org.bitcoinj.wallet.Protos;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/** /**
* Serialize and de-serialize a wallet to a protobuf stream. * Serialize and de-serialize a wallet to a protobuf stream.
* *
@ -59,7 +57,7 @@ public class WalletProtobufSerializer {
walletProto.writeTo(output); walletProto.writeTo(output);
} }
public static String walletToText(Wallet wallet) throws IOException { public static String walletToText(Wallet wallet) {
Protos.Wallet walletProto = walletToProto(wallet); Protos.Wallet walletProto = walletToProto(wallet);
return TextFormat.printToString(walletProto); return TextFormat.printToString(walletProto);
@ -128,23 +126,23 @@ public class WalletProtobufSerializer {
if (spentBy != null) { if (spentBy != null) {
outputBuilder outputBuilder
.setSpentByTransactionHash(ByteString.copyFrom(spentBy.getParentTransaction().getHash().getBytes())) .setSpentByTransactionHash(ByteString.copyFrom(spentBy.getParentTransaction().getHash().getBytes()))
.setSpentByTransactionIndex((int)spentBy.getParentTransaction().getInputs().indexOf(spentBy)); // FIXME .setSpentByTransactionIndex(spentBy.getParentTransaction().getInputs().indexOf(spentBy));
} }
txBuilder.addTransactionOutput(outputBuilder); txBuilder.addTransactionOutput(outputBuilder);
} }
// Handle which blocks tx was seen in // Handle which blocks tx was seen in
if (tx.getAppearsIn() != null) { if (tx.getAppearsInHashes() != null) {
for (StoredBlock block : tx.getAppearsIn()) { for (Sha256Hash hash : tx.getAppearsInHashes()) {
txBuilder.addBlockHash(ByteString.copyFrom(block.getHeader().getHash().getBytes())); txBuilder.addBlockHash(ByteString.copyFrom(hash.getBytes()));
} }
} }
return txBuilder.build(); return txBuilder.build();
} }
public static Wallet readWallet(InputStream input, NetworkParameters params, BlockStore store) public static Wallet readWallet(InputStream input, NetworkParameters params)
throws IOException, AddressFormatException, BlockStoreException { throws IOException {
WalletProtobufSerializer serializer = new WalletProtobufSerializer(); WalletProtobufSerializer serializer = new WalletProtobufSerializer();
Protos.Wallet walletProto = Protos.Wallet.parseFrom(input); Protos.Wallet walletProto = Protos.Wallet.parseFrom(input);
if (!params.getId().equals(walletProto.getNetworkIdentifier())) if (!params.getId().equals(walletProto.getNetworkIdentifier()))
@ -164,7 +162,7 @@ public class WalletProtobufSerializer {
// Read all transactions and create outputs // Read all transactions and create outputs
for (Protos.Transaction txProto : walletProto.getTransactionList()) { for (Protos.Transaction txProto : walletProto.getTransactionList()) {
serializer.readTransaction(txProto, params, store); serializer.readTransaction(txProto, params);
} }
// Create transactions inputs pointing to transactions // Create transactions inputs pointing to transactions
@ -188,8 +186,7 @@ public class WalletProtobufSerializer {
} }
private void readTransaction(Protos.Transaction txProto, private void readTransaction(Protos.Transaction txProto, NetworkParameters params) {
NetworkParameters params, BlockStore store) throws BlockStoreException {
Transaction tx = new Transaction(params, txProto.getVersion(), new Sha256Hash(txProto.getHash().toByteArray())); Transaction tx = new Transaction(params, txProto.getVersion(), new Sha256Hash(txProto.getHash().toByteArray()));
if (txProto.hasUpdatedAt()) if (txProto.hasUpdatedAt())
tx.setUpdateTime(new Date(txProto.getUpdatedAt())); tx.setUpdateTime(new Date(txProto.getUpdatedAt()));
@ -207,7 +204,7 @@ public class WalletProtobufSerializer {
} }
for (ByteString blockHash : txProto.getBlockHashList()) { for (ByteString blockHash : txProto.getBlockHashList()) {
tx.setBlockAppearance(store.get(new Sha256Hash(blockHash.toByteArray())), false); tx.addBlockAppearance(new Sha256Hash(blockHash.toByteArray()));
} }
if (txProto.hasLockTime()) { if (txProto.hasLockTime()) {

View File

@ -23,6 +23,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.HashSet;
import java.util.List; import java.util.List;
import static com.google.bitcoin.core.TestUtils.createFakeBlock; import static com.google.bitcoin.core.TestUtils.createFakeBlock;
@ -460,12 +461,10 @@ public class WalletTest {
// Verify we can handle the case of older wallets in which the timestamp is null (guessed from the // Verify we can handle the case of older wallets in which the timestamp is null (guessed from the
// block appearances list). // block appearances list).
tx1.updatedAt = null; tx1.updatedAt = null;
tx2.updatedAt = null; tx3.updatedAt = null;
// Check we got them back in order. // Check we got them back in order.
transactions = wallet.getTransactionsByTime(); transactions = wallet.getTransactionsByTime();
assertEquals(tx3, transactions.get(0)); assertEquals(tx2, transactions.get(0));
assertEquals(tx2, transactions.get(1));
assertEquals(tx1, transactions.get(2));
assertEquals(3, transactions.size()); assertEquals(3, transactions.size());
} }
@ -482,5 +481,18 @@ public class WalletTest {
wallet.addKey(new ECKey()); wallet.addKey(new ECKey());
assertEquals(now + 60, wallet.getEarliestKeyCreationTime()); assertEquals(now + 60, wallet.getEarliestKeyCreationTime());
} }
@Test
public void transactionAppearsInMigration() throws Exception {
// Test migration from appearsIn to appearsInHashes
Transaction tx1 = createFakeTx(params, Utils.toNanoCoins(1, 0), myAddress);
StoredBlock b1 = createFakeBlock(params, blockStore, tx1).storedBlock;
tx1.appearsIn = new HashSet<StoredBlock>();
tx1.appearsIn.add(b1);
assertEquals(1, tx1.getAppearsInHashes().size());
assertTrue(tx1.getAppearsInHashes().contains(b1.getHeader().getHash()));
assertNull(tx1.appearsIn);
}
// Support for offline spending is tested in PeerGroupTest // Support for offline spending is tested in PeerGroupTest
} }

View File

@ -3,18 +3,9 @@ package com.google.bitcoin.store;
import static com.google.bitcoin.core.TestUtils.createFakeTx; import static com.google.bitcoin.core.TestUtils.createFakeTx;
import static com.google.bitcoin.core.Utils.toNanoCoins; import static com.google.bitcoin.core.Utils.toNanoCoins;
import static org.junit.Assert.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import org.bitcoinj.wallet.Protos;
import org.junit.Before;
import org.junit.Test;
import com.google.bitcoin.core.Address; import com.google.bitcoin.core.Address;
import com.google.bitcoin.core.AddressFormatException;
import com.google.bitcoin.core.BlockChain; import com.google.bitcoin.core.BlockChain;
import com.google.bitcoin.core.ECKey; import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.NetworkParameters; import com.google.bitcoin.core.NetworkParameters;
@ -22,14 +13,20 @@ import com.google.bitcoin.core.Transaction;
import com.google.bitcoin.core.Utils; import com.google.bitcoin.core.Utils;
import com.google.bitcoin.core.Wallet; import com.google.bitcoin.core.Wallet;
import static org.junit.Assert.*; import org.bitcoinj.wallet.Protos;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
public class WalletProtobufSerializerTest { public class WalletProtobufSerializerTest {
static final NetworkParameters params = NetworkParameters.unitTests(); static final NetworkParameters params = NetworkParameters.unitTests();
private ECKey myKey; private ECKey myKey;
private Address myAddress; private Address myAddress;
private Wallet wallet; private Wallet wallet;
private MemoryBlockStore blockStore;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
@ -37,7 +34,6 @@ public class WalletProtobufSerializerTest {
myAddress = myKey.toAddress(params); myAddress = myKey.toAddress(params);
wallet = new Wallet(params); wallet = new Wallet(params);
wallet.addKey(myKey); wallet.addKey(myKey);
blockStore = new MemoryBlockStore(params);
} }
@Test @Test
@ -101,11 +97,11 @@ public class WalletProtobufSerializerTest {
} }
} }
private Wallet roundTrip(Wallet wallet) throws IOException, AddressFormatException, BlockStoreException { private Wallet roundTrip(Wallet wallet) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream(); ByteArrayOutputStream output = new ByteArrayOutputStream();
//System.out.println(WalletProtobufSerializer.walletToText(wallet)); //System.out.println(WalletProtobufSerializer.walletToText(wallet));
WalletProtobufSerializer.writeWallet(wallet, output); WalletProtobufSerializer.writeWallet(wallet, output);
ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray()); ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
return WalletProtobufSerializer.readWallet(input, params, blockStore); return WalletProtobufSerializer.readWallet(input, params);
} }
} }