From 6c31abd698aea42dcf0442ec63ff2e61a2847f2b Mon Sep 17 00:00:00 2001 From: Jim Burton Date: Tue, 3 Apr 2012 18:43:46 +0100 Subject: [PATCH] coinbase-tx phase1 complete (add blockheight to wallet and protobuf) --- .../java/com/google/bitcoin/core/Wallet.java | 25 +++++++++ .../store/WalletProtobufSerializer.java | 17 +++++- .../main/java/org/bitcoinj/wallet/Protos.java | 16 ++---- .../com/google/bitcoin/core/BlockTest.java | 2 +- .../com/google/bitcoin/core/TestUtils.java | 51 ++++++++++++++++- .../com/google/bitcoin/core/WalletTest.java | 56 +++++++++++++++++++ .../store/WalletProtobufSerializerTest.java | 28 ++++++++++ 7 files changed, 177 insertions(+), 18 deletions(-) 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 74323330..debb21fa 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -144,6 +144,11 @@ public class Wallet implements Serializable { // this field) then we need to migrate. private boolean hasTransactionConfidences; + /** + * The hash of the last block seen on the best chain + */ + private Sha256Hash lastBlockSeenHash; + transient private ArrayList eventListeners; /** @@ -533,6 +538,18 @@ 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. @@ -1640,4 +1657,12 @@ public class Wallet implements Serializable { } return peerEventListener; } + + public Sha256Hash getLastBlockSeenHash() { + return lastBlockSeenHash; + } + + public void setLastBlockSeenHash(Sha256Hash lastBlockSeenHash) { + this.lastBlockSeenHash = lastBlockSeenHash; + } } 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 9f14d810..9673fa25 100644 --- a/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java +++ b/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java @@ -90,9 +90,7 @@ public class WalletProtobufSerializer { */ public static Protos.Wallet walletToProto(Wallet wallet) { Protos.Wallet.Builder walletBuilder = Protos.Wallet.newBuilder(); - walletBuilder.setNetworkIdentifier(wallet.getNetworkParameters().getId()) - //.setLastSeenBlockHash(null) // TODO - ; + walletBuilder.setNetworkIdentifier(wallet.getNetworkParameters().getId()); for (WalletTransaction wtx : wallet.getWalletTransactions()) { Protos.Transaction txProto = makeTxProto(wtx); walletBuilder.addTransaction(txProto); @@ -110,6 +108,12 @@ public class WalletProtobufSerializer { buf.setPublicKey(ByteString.copyFrom(key.getPubKey())); walletBuilder.addKey(buf); } + + Sha256Hash lastSeenBlockHash = wallet.getLastBlockSeenHash(); + if (lastSeenBlockHash != null) { + walletBuilder.setLastSeenBlockHash(hashToByteString(lastSeenBlockHash)); + } + return walletBuilder.build(); } @@ -238,6 +242,13 @@ public class WalletProtobufSerializer { wallet.addWalletTransaction(wtx); } + // Update the lastBlockSeenHash. + if (!walletProto.hasLastSeenBlockHash()) { + wallet.setLastBlockSeenHash(null); + } else { + wallet.setLastBlockSeenHash(byteStringToHash(walletProto.getLastSeenBlockHash())); + } + for (Protos.Extension extProto : walletProto.getExtensionList()) { if (extProto.getMandatory()) { throw new IllegalArgumentException("Did not understand a mandatory extension in the wallet"); diff --git a/core/src/main/java/org/bitcoinj/wallet/Protos.java b/core/src/main/java/org/bitcoinj/wallet/Protos.java index 36541dd0..be1dac1b 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Protos.java +++ b/core/src/main/java/org/bitcoinj/wallet/Protos.java @@ -2323,7 +2323,7 @@ public final class Protos { boolean hasHash(); com.google.protobuf.ByteString getHash(); - // required .wallet.Transaction.Pool pool = 3; + // optional .wallet.Transaction.Pool pool = 3; boolean hasPool(); org.bitcoinj.wallet.Protos.Transaction.Pool getPool(); @@ -2495,7 +2495,7 @@ public final class Protos { return hash_; } - // required .wallet.Transaction.Pool pool = 3; + // optional .wallet.Transaction.Pool pool = 3; public static final int POOL_FIELD_NUMBER = 3; private org.bitcoinj.wallet.Protos.Transaction.Pool pool_; public boolean hasPool() { @@ -2618,10 +2618,6 @@ public final class Protos { memoizedIsInitialized = 0; return false; } - if (!hasPool()) { - memoizedIsInitialized = 0; - return false; - } for (int i = 0; i < getTransactionInputCount(); i++) { if (!getTransactionInput(i).isInitialized()) { memoizedIsInitialized = 0; @@ -3073,10 +3069,6 @@ public final class Protos { return false; } - if (!hasPool()) { - - return false; - } for (int i = 0; i < getTransactionInputCount(); i++) { if (!getTransactionInput(i).isInitialized()) { @@ -3223,7 +3215,7 @@ public final class Protos { return this; } - // required .wallet.Transaction.Pool pool = 3; + // optional .wallet.Transaction.Pool pool = 3; private org.bitcoinj.wallet.Protos.Transaction.Pool pool_ = org.bitcoinj.wallet.Protos.Transaction.Pool.UNSPENT; public boolean hasPool() { return ((bitField0_ & 0x00000004) == 0x00000004); @@ -5689,7 +5681,7 @@ public final class Protos { "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 \002(\0162\030.wallet.Transact" + + "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" + diff --git a/core/src/test/java/com/google/bitcoin/core/BlockTest.java b/core/src/test/java/com/google/bitcoin/core/BlockTest.java index 639c1071..62e741c5 100644 --- a/core/src/test/java/com/google/bitcoin/core/BlockTest.java +++ b/core/src/test/java/com/google/bitcoin/core/BlockTest.java @@ -31,7 +31,7 @@ import static org.junit.Assert.*; public class BlockTest { static final NetworkParameters params = NetworkParameters.testNet(); - static final byte[] blockBytes; + public static final byte[] blockBytes; static { // Block 00000000a6e5eb79dcec11897af55e90cd571a4335383a3ccfbc12ec81085935 diff --git a/core/src/test/java/com/google/bitcoin/core/TestUtils.java b/core/src/test/java/com/google/bitcoin/core/TestUtils.java index d12b3f0f..a023ba01 100644 --- a/core/src/test/java/com/google/bitcoin/core/TestUtils.java +++ b/core/src/test/java/com/google/bitcoin/core/TestUtils.java @@ -19,10 +19,13 @@ package com.google.bitcoin.core; import com.google.bitcoin.store.BlockStore; import com.google.bitcoin.store.BlockStoreException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.math.BigInteger; public class TestUtils { - public static Transaction createFakeTx(NetworkParameters params, BigInteger nanocoins, Address to) { + public static Transaction createFakeTx(NetworkParameters params, BigInteger nanocoins, Address to) throws IOException, ProtocolException { // Create a fake TX of sufficient realism to exercise the unit tests. Two outputs, one to us, one to somewhere // else to simulate change. Transaction t = new Transaction(params); @@ -38,7 +41,51 @@ public class TestUtils { prevTx.addOutput(prevOut); // Connect it. t.addInput(prevOut); - return t; + + // roundtrip tx + return roundTripTransaction(params, t); + } + + /** + * @return Transaction[] Transaction[0] is a feeder transaction, supplying BTC to Transaction[1] + */ + public static Transaction[] createFakeTx(NetworkParameters params, BigInteger nanocoins, Address to, Address from) throws IOException, ProtocolException { + // Create fake TXes of sufficient realism to exercise the unit tests. This transaction send BTC from the from address, to the to address + // with to one to somewhere else to simulate change. + Transaction t = new Transaction(params); + TransactionOutput outputToMe = new TransactionOutput(params, t, nanocoins, to); + t.addOutput(outputToMe); + TransactionOutput change = new TransactionOutput(params, t, Utils.toNanoCoins(1, 11), new ECKey().toAddress(params)); + t.addOutput(change); + // Make a feeder tx that sends to the from address specified. This feeder tx is not really valid but it doesn't + // matter for our purposes. + Transaction feederTx = new Transaction(params); + TransactionOutput feederOut = new TransactionOutput(params, feederTx, nanocoins, from); + feederTx.addOutput(feederOut); + + // make a previous tx that sends from the feeder to the from address + Transaction prevTx = new Transaction(params); + TransactionOutput prevOut = new TransactionOutput(params, prevTx, nanocoins, to); + prevTx.addOutput(prevOut); + + // Connect up the txes + prevTx.addInput(feederOut); + t.addInput(prevOut); + + // roundtrip the tx so that they are just like they would be from the wire + return new Transaction[]{roundTripTransaction(params, prevTx), roundTripTransaction(params,t)}; + } + + /** + * Roundtrip a transaction so that it appears as if it has just come from the wire + */ + private static Transaction roundTripTransaction(NetworkParameters params, Transaction tx) throws IOException, ProtocolException { + BitcoinSerializer bs = new BitcoinSerializer(params, true, null); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bs.serialize(tx, bos); + + return (Transaction)bs.deserialize(new ByteArrayInputStream(bos.toByteArray())); } public static class DoubleSpends { 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 900e368a..43173b50 100644 --- a/core/src/test/java/com/google/bitcoin/core/WalletTest.java +++ b/core/src/test/java/com/google/bitcoin/core/WalletTest.java @@ -656,5 +656,61 @@ public class WalletTest { } + @Test + public void spendToSameWallet() throws Exception { + // Test that a spend to the same wallet is dealt with correctly. + // It should appear in the wallet and confirm. + // This is a bit of a silly thing to do in the real world as all it does is burn a fee but it is perfectly valid. + + BigInteger coin1 = Utils.toNanoCoins(1, 0); + BigInteger coinHalf = Utils.toNanoCoins(0, 50); + + // Start by giving us 1 coin. + Transaction inbound1 = createFakeTx(params, coin1, myAddress); + wallet.receiveFromBlock(inbound1, null, BlockChain.NewBlockType.BEST_CHAIN); + + // Send half to ourselves. We should then have a balance available to spend of zero. + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.ALL)); + + Transaction outbound1 = wallet.createSend(myAddress, coinHalf); + wallet.commitTx(outbound1); + + // We should have a zero available balance before the next block. + assertEquals(BigInteger.ZERO, wallet.getBalance()); + + wallet.receiveFromBlock(outbound1, null, BlockChain.NewBlockType.BEST_CHAIN); + + // We should have a balance of 1 BTC after the block is received. + assertEquals(coin1, wallet.getBalance()); + } + + @Test + public void rememberLastBlockSeenHash() throws Exception { + BigInteger v1 = toNanoCoins(5, 0); + BigInteger v2 = toNanoCoins(0, 50); + BigInteger v3 = toNanoCoins(0, 25); + Transaction t1 = createFakeTx(params, v1, myAddress); + Transaction t2 = createFakeTx(params, v2, myAddress); + Transaction t3 = createFakeTx(params, v3, myAddress); + StoredBlock b1 = createFakeBlock(params, blockStore, t1).storedBlock; + // TODO for Mike - b2 gets sent to side chain but b1, b2, b3 are all chained together - unrealistic. + StoredBlock b2 = createFakeBlock(params, blockStore, t2).storedBlock; + StoredBlock b3 = createFakeBlock(params, blockStore, t3).storedBlock; + + // Receive a block on the best chain - this should set the last block seen hash. + wallet.receiveFromBlock(t1, b1, BlockChain.NewBlockType.BEST_CHAIN); + assertEquals(b1.getHeader().getHash(), wallet.getLastBlockSeenHash()); + + // Receive a block on the side chain - this should not change the last block seen hash. + wallet.receiveFromBlock(t2, b2, BlockChain.NewBlockType.SIDE_CHAIN); + assertEquals(b1.getHeader().getHash(), wallet.getLastBlockSeenHash()); + + // Receive block 3 on the best chain - this should change the last block seen hash. + wallet.receiveFromBlock(t3, b3, BlockChain.NewBlockType.BEST_CHAIN); + assertEquals(b3.getHeader().getHash(), wallet.getLastBlockSeenHash()); + } + + // Support for offline spending is tested in PeerGroupTest } 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 f62f5664..9abae687 100644 --- a/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java +++ b/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java @@ -4,6 +4,8 @@ package com.google.bitcoin.store; import com.google.bitcoin.core.*; import com.google.bitcoin.core.TransactionConfidence.ConfidenceType; import com.google.bitcoin.utils.BriefLogFormatter; +import com.google.protobuf.ByteString; + import org.bitcoinj.wallet.Protos; import org.junit.Before; import org.junit.Test; @@ -108,6 +110,32 @@ public class WalletProtobufSerializerTest { } } + @Test + public void testLastBlockSeenHash() throws Exception { + // Test the lastBlockSeenHash field works. + + // LastBlockSeenHash should be empty if never set. + wallet = new Wallet(params); + Protos.Wallet walletProto = WalletProtobufSerializer.walletToProto(wallet); + ByteString lastSeenBlockHash = walletProto.getLastSeenBlockHash(); + assertTrue(lastSeenBlockHash.isEmpty()); + + // Create a block. + Block block = new Block(params, BlockTest.blockBytes); + Sha256Hash blockHash = block.getHash(); + wallet.setLastBlockSeenHash(blockHash); + + // Roundtrip the wallet and check it has stored te blockHash. + Wallet wallet1 = roundTrip(wallet); + assertEquals(blockHash, wallet1.getLastBlockSeenHash()); + + // Test the Satoshi genesis block (hash of all zeroes) is roundtripped ok. + Block genesisBlock = NetworkParameters.prodNet().genesisBlock; + wallet.setLastBlockSeenHash(genesisBlock.getHash()); + Wallet wallet2 = roundTrip(wallet); + assertEquals(genesisBlock.getHash(), wallet2.getLastBlockSeenHash()); + } + private Wallet roundTrip(Wallet wallet) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); //System.out.println(WalletProtobufSerializer.walletToText(wallet));