From 6af16c863c28117e896c61e648082191d8510ae2 Mon Sep 17 00:00:00 2001 From: Miron Cuperman Date: Fri, 6 Jan 2012 14:50:34 -0800 Subject: [PATCH] Protobuf serialization for Wallet --- pom.xml | 56 ++++++++++ src/bitcoin.proto | 80 ++++++++++++++ .../bitcoin/core/NetworkParameters.java | 12 +++ src/com/google/bitcoin/core/Transaction.java | 5 +- .../bitcoin/core/TransactionOutput.java | 2 +- src/com/google/bitcoin/core/Wallet.java | 80 +++++++++++--- .../bitcoin/core/WalletTransaction.java | 58 ++++++++++ .../store/WalletProtobufSerializer.java | 102 ++++++++++++++++++ tests/com/google/bitcoin/core/WalletTest.java | 35 +++--- 9 files changed, 399 insertions(+), 31 deletions(-) create mode 100644 src/bitcoin.proto create mode 100644 src/com/google/bitcoin/core/WalletTransaction.java create mode 100644 src/com/google/bitcoin/store/WalletProtobufSerializer.java diff --git a/pom.xml b/pom.xml index 32002470..65252472 100644 --- a/pom.xml +++ b/pom.xml @@ -213,6 +213,54 @@ + + maven-antrun-plugin + + + compile-protoc + generate-sources + + + + + + + + + + + + + + + + + + run + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 1.1 + + + add-source + generate-sources + + add-source + + + + gen + + + + + + @@ -260,6 +308,12 @@ bcprov-jdk15 ${bcprov-jdk15.version} + + + com.google.protobuf + protobuf-java + ${protobuf.version} + @@ -270,6 +324,8 @@ 4.8.2 1.6.2 10.8.2.2 + 2.2.0 + gen diff --git a/src/bitcoin.proto b/src/bitcoin.proto new file mode 100644 index 00000000..338971d6 --- /dev/null +++ b/src/bitcoin.proto @@ -0,0 +1,80 @@ +/** + * Copyright 2012 Google Inc. + * + * 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. + */ + +/* + * Author: Jim Burton + */ + +package wallet; + +option java_package = "org.bitcoinj.wallet"; +option java_outer_classname = "Protos"; + +message Wallet { + required string network_identifier = 1; // the network used by this wallet + // org.bitcoin.production = production network (Satoshi genesis block) + // org.bitcoin.test = test network (Andresen genesis block) + + optional bytes last_seen_block_hash = 2; // the Sha256 hash of the block last seen by this wallet + + message Key { + required string private_key = 1; // base58 representation of private key + optional string label = 2; // for presentation purposes + optional int64 creation_timestamp = 3; // datetime stored as millis since epoch. + } + repeated Key key = 3; + + + message Transaction { + enum Pool { + UNSPENT = 0; + SPENT = 1; + PENDING = 2; + INACTIVE = 3; + DEAD = 4; + } + + // See com.google.bitcoin.core.Wallet.java for detailed description of pool semantics + required Pool pool = 1; + + optional int64 updated_at = 2; // millis since epoch the transaction was last updated + + message TransactionInput { + required bytes transaction_out_point_hash = 1; + // Sha256Hash of transaction output this input is using + required int32 transaction_out_point_index = 2; + // index of transaction output used by this input if in this wallet + + required bytes script_bytes = 3; // script of transaction input + } + + repeated TransactionInput transaction_input = 3; + + message TransactionOutput { + required int64 value = 1; + required bytes script_bytes = 2; // script of transaction output + optional bytes spent_by_transaction_hash = 3; // if spent, the Sha256Hash of the transaction doing the spend + optional int32 spent_by_transaction_index = 4; + // if spent, the index of the transaction output of the transaction doing the spend + } + repeated TransactionOutput transaction_output = 4; + + + repeated bytes block_hash = 5; + // Sha256Hash of block in block chain in which this transaction appears + } + repeated Transaction transaction = 4; +} // end of Wallet diff --git a/src/com/google/bitcoin/core/NetworkParameters.java b/src/com/google/bitcoin/core/NetworkParameters.java index 434d1a46..08853f8c 100644 --- a/src/com/google/bitcoin/core/NetworkParameters.java +++ b/src/com/google/bitcoin/core/NetworkParameters.java @@ -78,6 +78,8 @@ public class NetworkParameters implements Serializable { * signatures using it. */ public byte[] alertSigningKey; + + public String id; private static Block createGenesis(NetworkParameters n) { Block genesisBlock = new Block(n); @@ -122,6 +124,7 @@ public class NetworkParameters implements Serializable { n.genesisBlock.setTime(1296688602L); n.genesisBlock.setDifficultyTarget(0x1d07fff8L); n.genesisBlock.setNonce(384568319); + n.id = "org.bitcoin.test"; String genesisHash = n.genesisBlock.getHashAsString(); assert genesisHash.equals("00000007199508e34a9ff81e6ec0c477a4cccff2a4767a8eee39c11db367b008") : genesisHash; return n; @@ -148,6 +151,7 @@ public class NetworkParameters implements Serializable { n.genesisBlock.setDifficultyTarget(0x1d00ffffL); n.genesisBlock.setTime(1231006505L); n.genesisBlock.setNonce(2083236893); + n.id = "org.bitcoin.production"; String genesisHash = n.genesisBlock.getHashAsString(); assert genesisHash.equals("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f") : genesisHash; return n; @@ -162,6 +166,14 @@ public class NetworkParameters implements Serializable { n.genesisBlock.setDifficultyTarget(Block.EASIEST_DIFFICULTY_TARGET); n.interval = 10; n.targetTimespan = 200000000; // 6 years. Just a very big number. + n.id = "com.google.bitcoin.unittest"; return n; } + + /** + * A java package style string acting as unique ID for these parameters + */ + public String getId() { + return id; + } } diff --git a/src/com/google/bitcoin/core/Transaction.java b/src/com/google/bitcoin/core/Transaction.java index 432e2560..6805a7a1 100644 --- a/src/com/google/bitcoin/core/Transaction.java +++ b/src/com/google/bitcoin/core/Transaction.java @@ -180,7 +180,7 @@ 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 * because it's not stored in the wallet or because it has never appeared in a block. */ - Set getAppearsIn() { + public Set getAppearsIn() { return appearsIn; } @@ -204,6 +204,9 @@ public class Transaction extends ChildMessage implements Serializable { * @param bestChain whether to set the updatedAt timestamp from the block header (only if not already set) */ void setBlockAppearance(StoredBlock block, boolean bestChain) { + if (bestChain && updatedAt == null) { + updatedAt = new Date(block.getHeader().getTimeSeconds() * 1000); + } if (appearsIn == null) { appearsIn = new HashSet(); } diff --git a/src/com/google/bitcoin/core/TransactionOutput.java b/src/com/google/bitcoin/core/TransactionOutput.java index 2143a95a..afbe224c 100644 --- a/src/com/google/bitcoin/core/TransactionOutput.java +++ b/src/com/google/bitcoin/core/TransactionOutput.java @@ -205,7 +205,7 @@ public class TransactionOutput extends ChildMessage implements Serializable { /** * Returns the connected input. */ - TransactionInput getSpentBy() { + public TransactionInput getSpentBy() { return spentBy; } diff --git a/src/com/google/bitcoin/core/Wallet.java b/src/com/google/bitcoin/core/Wallet.java index d25735c8..75020caa 100644 --- a/src/com/google/bitcoin/core/Wallet.java +++ b/src/com/google/bitcoin/core/Wallet.java @@ -16,6 +16,8 @@ package com.google.bitcoin.core; +import com.google.bitcoin.core.WalletTransaction.Pool; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -150,6 +152,14 @@ public class Wallet implements Serializable { dead = new HashMap(); eventListeners = new ArrayList(); } + + public NetworkParameters getNetworkParameters() { + return params; + } + + public Iterable getKeys() { + return keychain; + } /** * Uses Java serialization to save the wallet to the given file. @@ -185,6 +195,12 @@ public class Wallet implements Serializable { public static Wallet loadFromFile(File f) throws IOException { return loadFromFileStream(new FileInputStream(f)); } + + private void checkInvariants() { + if (getTransactions(true, true).size() != + unspent.size() + spent.size() + pending.size() + dead.size() + inactive.size()) + throw new RuntimeException("Invariant broken - a tx appears in more than one pool"); + } /** * Returns a wallet deserialized from the given file input stream. @@ -429,6 +445,8 @@ public class Wallet implements Serializable { if (!reorg && bestChain && valueDifference.compareTo(BigInteger.ZERO) > 0 && wtx == null) { invokeOnCoinsReceived(tx, prevBalance, getBalance()); } + + checkInvariants(); } /** @@ -604,6 +622,8 @@ public class Wallet implements Serializable { // Add to the pending pool. It'll be moved out once we receive this transaction on the best chain. log.info("->pending: {}", tx.getHashAsString()); pending.put(tx.getHash(), tx); + + checkInvariants(); } /** @@ -624,6 +644,48 @@ public class Wallet implements Serializable { return all; } + /** + * Returns a set of all WalletTransactions in the wallet. + */ + public Iterable getWalletTransactions() { + Set all = new HashSet(); + addWalletTransactionsToSet(all, Pool.UNSPENT, unspent); + addWalletTransactionsToSet(all, Pool.SPENT, spent); + addWalletTransactionsToSet(all, Pool.PENDING, pending); + addWalletTransactionsToSet(all, Pool.DEAD, dead); + addWalletTransactionsToSet(all, Pool.INACTIVE, inactive); + return all; + } + + static private void addWalletTransactionsToSet(Set txs, + Pool poolType, Map pool) { + for (Transaction tx : pool.values()) { + txs.add(new WalletTransaction(poolType, tx)); + } + } + + public void addWalletTransaction(WalletTransaction wtx) { + switch (wtx.getPool()) { + case UNSPENT: + unspent.put(wtx.getTransaction().getHash(), wtx.getTransaction()); + break; + case SPENT: + spent.put(wtx.getTransaction().getHash(), wtx.getTransaction()); + break; + case PENDING: + pending.put(wtx.getTransaction().getHash(), wtx.getTransaction()); + break; + case DEAD: + dead.put(wtx.getTransaction().getHash(), wtx.getTransaction()); + break; + case INACTIVE: + inactive.put(wtx.getTransaction().getHash(), wtx.getTransaction()); + break; + default: + throw new RuntimeException("Unknown wallet transaction type " + wtx.getPool()); + } + } + /** * Returns all non-dead, active transactions ordered by recency. */ @@ -642,7 +704,9 @@ public class Wallet implements Serializable { public List getRecentTransactions(int numTransactions, boolean includeDead) { assert numTransactions >= 0; // Firstly, put all transactions into an array. - int size = getPoolSize(Pool.UNSPENT) + getPoolSize(Pool.SPENT) + getPoolSize(Pool.PENDING); + int size = getPoolSize(WalletTransaction.Pool.UNSPENT) + + getPoolSize(WalletTransaction.Pool.SPENT) + + getPoolSize(WalletTransaction.Pool.PENDING); if (numTransactions > size || numTransactions == 0) { numTransactions = size; } @@ -695,16 +759,6 @@ public class Wallet implements Serializable { } } - // This is used only for unit testing, it's an internal API. - enum Pool { - UNSPENT, - SPENT, - PENDING, - INACTIVE, - DEAD, - ALL, - } - EnumSet getContainingPools(Transaction tx) { EnumSet result = EnumSet.noneOf(Pool.class); Sha256Hash txHash = tx.getHash(); @@ -726,7 +780,7 @@ public class Wallet implements Serializable { return result; } - int getPoolSize(Pool pool) { + int getPoolSize(WalletTransaction.Pool pool) { switch (pool) { case UNSPENT: return unspent.size(); @@ -1218,6 +1272,8 @@ public class Wallet implements Serializable { l.onReorganize(this); } } + + checkInvariants(); } private void reprocessTxAfterReorg(Map pool, Transaction tx) { diff --git a/src/com/google/bitcoin/core/WalletTransaction.java b/src/com/google/bitcoin/core/WalletTransaction.java new file mode 100644 index 00000000..ddf53a83 --- /dev/null +++ b/src/com/google/bitcoin/core/WalletTransaction.java @@ -0,0 +1,58 @@ +/** + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.core; + +/** + * A Transaction in a Wallet - includes the pool ID + * + * @author Miron Cuperman + */ +public class WalletTransaction { + public enum Pool { + UNSPENT(0), + SPENT(1), + PENDING(2), + INACTIVE(3), + DEAD(4), + ALL(-1); + + private int value; + Pool(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + private Transaction transaction; + private Pool pool; + + public WalletTransaction(Pool pool, Transaction transaction) { + this.pool = pool; + this.transaction = transaction; + } + + public Transaction getTransaction() { + return transaction; + } + + public Pool getPool() { + return pool; + } +} + diff --git a/src/com/google/bitcoin/store/WalletProtobufSerializer.java b/src/com/google/bitcoin/store/WalletProtobufSerializer.java new file mode 100644 index 00000000..41657d5c --- /dev/null +++ b/src/com/google/bitcoin/store/WalletProtobufSerializer.java @@ -0,0 +1,102 @@ +/** + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.store; + +import com.google.bitcoin.core.ECKey; +import com.google.bitcoin.core.StoredBlock; +import com.google.bitcoin.core.Transaction; +import com.google.bitcoin.core.TransactionInput; +import com.google.bitcoin.core.TransactionOutput; +import com.google.bitcoin.core.Wallet; +import com.google.bitcoin.core.WalletTransaction; +import com.google.protobuf.ByteString; + +import org.bitcoinj.wallet.Protos; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Serialize and de-serialize a wallet to a protobuf stream. + * + * @author Miron Cuperman + */ +public class WalletProtobufSerializer { + void writeWallet(Wallet wallet, OutputStream output) throws IOException { + Protos.Wallet.Builder walletBuilder = Protos.Wallet.newBuilder(); + walletBuilder + .setNetworkIdentifier(wallet.getNetworkParameters().getId()) + .setLastSeenBlockHash(null) // TODO + ; + for (WalletTransaction wtx : wallet.getWalletTransactions()) { + Protos.Wallet.Transaction txProto = makeTxProto(wtx); + walletBuilder.addTransaction(txProto); + } + + for (ECKey key : wallet.getKeys()) { + final String base58PrivateKey = + key.getPrivateKeyEncoded(wallet.getNetworkParameters()).toString(); + walletBuilder.addKey( + Protos.Wallet.Key.newBuilder() + // .setCreationTimestamp() TODO + // .setLabel() TODO + .setPrivateKey(base58PrivateKey)); + } + + walletBuilder.build().writeTo(output); + } + + private Protos.Wallet.Transaction makeTxProto(WalletTransaction wtx) { + Transaction tx = wtx.getTransaction(); + Protos.Wallet.Transaction.Builder txBuilder = Protos.Wallet.Transaction.newBuilder(); + + txBuilder + .setUpdatedAt(tx.getUpdateTime().getTime()) + .setPool(Protos.Wallet.Transaction.Pool.valueOf(wtx.getPool().getValue())); + + // Handle inputs + for (TransactionInput input : tx.getInputs()) { + txBuilder.addTransactionInput( + Protos.Wallet.Transaction.TransactionInput.newBuilder() + .setScriptBytes(ByteString.copyFrom(input.getScriptBytes())) + .setTransactionOutPointHash(ByteString.copyFrom( + input.getOutpoint().getHash().getBytes())) + .setTransactionOutPointIndex((int)input.getOutpoint().getIndex()) // FIXME + ); + } + + // Handle outputs + for (TransactionOutput output : tx.getOutputs()) { + final TransactionInput spentBy = output.getSpentBy(); + txBuilder.addTransactionOutput( + Protos.Wallet.Transaction.TransactionOutput.newBuilder() + .setScriptBytes(ByteString.copyFrom(output.getScriptBytes())) + .setSpentByTransactionHash(ByteString.copyFrom( + spentBy.getHash().getBytes())) + .setSpentByTransactionIndex((int)spentBy.getOutpoint().getIndex()) // FIXME + .setValue(output.getValue().longValue()) + ); + } + + // Handle which blocks tx was seen in + for (StoredBlock block : tx.getAppearsIn()) { + txBuilder.addBlockHash(ByteString.copyFrom(block.getHeader().getHash().getBytes())); + } + + return txBuilder.build(); + } +} diff --git a/tests/com/google/bitcoin/core/WalletTest.java b/tests/com/google/bitcoin/core/WalletTest.java index 88e207a2..cc725573 100644 --- a/tests/com/google/bitcoin/core/WalletTest.java +++ b/tests/com/google/bitcoin/core/WalletTest.java @@ -61,14 +61,14 @@ public class WalletTest { wallet.receiveFromBlock(t1, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(v1, wallet.getBalance()); - assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT)); - assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.ALL)); ECKey k2 = new ECKey(); BigInteger v2 = toNanoCoins(0, 50); Transaction t2 = wallet.createSend(k2.toAddress(params), v2); - assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT)); - assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.ALL)); // Do some basic sanity checks. assertEquals(1, t2.getInputs().size()); @@ -77,9 +77,9 @@ public class WalletTest { // We have NOT proven that the signature is correct! wallet.commitTx(t2); - assertEquals(1, wallet.getPoolSize(Wallet.Pool.PENDING)); - assertEquals(1, wallet.getPoolSize(Wallet.Pool.SPENT)); - assertEquals(2, wallet.getPoolSize(Wallet.Pool.ALL)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.PENDING)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.SPENT)); + assertEquals(2, wallet.getPoolSize(WalletTransaction.Pool.ALL)); } @Test @@ -90,14 +90,14 @@ public class WalletTest { wallet.receiveFromBlock(t1, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(v1, wallet.getBalance()); - assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT)); - assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.ALL)); BigInteger v2 = toNanoCoins(0, 50); Transaction t2 = createFakeTx(params, v2, myAddress); wallet.receiveFromBlock(t2, null, BlockChain.NewBlockType.SIDE_CHAIN); - assertEquals(1, wallet.getPoolSize(Wallet.Pool.INACTIVE)); - assertEquals(2, wallet.getPoolSize(Wallet.Pool.ALL)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.INACTIVE)); + assertEquals(2, wallet.getPoolSize(WalletTransaction.Pool.ALL)); assertEquals(v1, wallet.getBalance()); } @@ -107,6 +107,7 @@ public class WalletTest { final Transaction fakeTx = createFakeTx(params, Utils.toNanoCoins(1, 0), myAddress); final boolean[] didRun = new boolean[1]; WalletEventListener listener = new AbstractWalletEventListener() { + @Override public void onCoinsReceived(Wallet w, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { assertTrue(prevBalance.equals(BigInteger.ZERO)); assertTrue(newBalance.equals(Utils.toNanoCoins(1, 0))); @@ -130,18 +131,18 @@ public class WalletTest { StoredBlock b1 = createFakeBlock(params, blockStore, t1).storedBlock; StoredBlock b2 = createFakeBlock(params, blockStore, t2).storedBlock; BigInteger expected = toNanoCoins(5, 50); - assertEquals(0, wallet.getPoolSize(Wallet.Pool.ALL)); + assertEquals(0, wallet.getPoolSize(WalletTransaction.Pool.ALL)); wallet.receiveFromBlock(t1, b1, BlockChain.NewBlockType.BEST_CHAIN); - assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT)); wallet.receiveFromBlock(t2, b2, BlockChain.NewBlockType.BEST_CHAIN); - assertEquals(2, wallet.getPoolSize(Wallet.Pool.UNSPENT)); + assertEquals(2, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT)); assertEquals(expected, wallet.getBalance()); // Now spend one coin. BigInteger v3 = toNanoCoins(1, 0); Transaction spend = wallet.createSend(new ECKey().toAddress(params), v3); wallet.commitTx(spend); - assertEquals(1, wallet.getPoolSize(Wallet.Pool.PENDING)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.PENDING)); // Available and estimated balances should not be the same. We don't check the exact available balance here // because it depends on the coin selection algorithm. @@ -229,8 +230,8 @@ public class WalletTest { wallet.receiveFromBlock(inbound1, null, BlockChain.NewBlockType.BEST_CHAIN); // Send half to some other guy. Sending only half then waiting for a confirm is important to ensure the tx is // in the unspent pool, not pending or spent. - assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT)); - assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.ALL)); Address someOtherGuy = new ECKey().toAddress(params); Transaction outbound1 = wallet.createSend(someOtherGuy, coinHalf); wallet.commitTx(outbound1);