From 319c52b2a601670fc4d7d7d00afe7ae7841d592f Mon Sep 17 00:00:00 2001 From: Miron Cuperman Date: Fri, 6 Jan 2012 16:31:43 -0800 Subject: [PATCH] Read Wallet from protobuf stream --- src/bitcoin.proto | 11 +- src/com/google/bitcoin/core/Transaction.java | 16 +- .../google/bitcoin/core/TransactionInput.java | 12 ++ .../bitcoin/core/TransactionOutPoint.java | 2 +- .../bitcoin/core/TransactionOutput.java | 11 +- .../bitcoin/core/WalletTransaction.java | 12 ++ .../store/WalletProtobufSerializer.java | 141 ++++++++++++++++-- 7 files changed, 185 insertions(+), 20 deletions(-) diff --git a/src/bitcoin.proto b/src/bitcoin.proto index 338971d6..835f7409 100644 --- a/src/bitcoin.proto +++ b/src/bitcoin.proto @@ -48,9 +48,10 @@ message Wallet { } // See com.google.bitcoin.core.Wallet.java for detailed description of pool semantics - required Pool pool = 1; + required bytes hash = 1; + required Pool pool = 2; - optional int64 updated_at = 2; // millis since epoch the transaction was last updated + optional int64 updated_at = 3; // millis since epoch the transaction was last updated message TransactionInput { required bytes transaction_out_point_hash = 1; @@ -61,7 +62,7 @@ message Wallet { required bytes script_bytes = 3; // script of transaction input } - repeated TransactionInput transaction_input = 3; + repeated TransactionInput transaction_input = 4; message TransactionOutput { required int64 value = 1; @@ -70,10 +71,10 @@ message Wallet { 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 TransactionOutput transaction_output = 5; - repeated bytes block_hash = 5; + repeated bytes block_hash = 6; // Sha256Hash of block in block chain in which this transaction appears } repeated Transaction transaction = 4; diff --git a/src/com/google/bitcoin/core/Transaction.java b/src/com/google/bitcoin/core/Transaction.java index 6805a7a1..faecf7c8 100644 --- a/src/com/google/bitcoin/core/Transaction.java +++ b/src/com/google/bitcoin/core/Transaction.java @@ -78,7 +78,7 @@ public class Transaction extends ChildMessage implements Serializable { // Data about how confirmed this tx is. Serialized, may be null. private TransactionConfidence confidence; - Transaction(NetworkParameters params) { + public Transaction(NetworkParameters params) { super(params); version = 1; inputs = new ArrayList(); @@ -87,6 +87,16 @@ public class Transaction extends ChildMessage implements Serializable { length = 10; // 8 for std fields + 1 for each 0 varint } + public Transaction(NetworkParameters params, Sha256Hash hash) { + super(params); + version = 1; + inputs = new ArrayList(); + outputs = new ArrayList(); + this.hash = hash; + // We don't initialize appearsIn deliberately as it's only useful for transactions stored in the wallet. + length = 10; //8 for std fields + 1 for each 0 varint + } + /** * Creates a transaction from the given serialized bytes, eg, from a block or a tx network message. */ @@ -337,6 +347,10 @@ public class Transaction extends ChildMessage implements Serializable { } return updatedAt; } + + public void setUpdateTime(Date updatedAt) { + this.updatedAt = updatedAt; + } /** * These constants are a part of a scriptSig signature on the inputs. They define the details of how a diff --git a/src/com/google/bitcoin/core/TransactionInput.java b/src/com/google/bitcoin/core/TransactionInput.java index b174866b..e2825278 100644 --- a/src/com/google/bitcoin/core/TransactionInput.java +++ b/src/com/google/bitcoin/core/TransactionInput.java @@ -61,6 +61,18 @@ public class TransactionInput extends ChildMessage implements Serializable { length = 40 + (scriptBytes == null ? 1 : VarInt.sizeOf(scriptBytes.length) + scriptBytes.length); } + public TransactionInput(NetworkParameters params, Transaction parentTransaction, + byte[] scriptBytes, + TransactionOutPoint outpoint) { + super(params); + this.scriptBytes = scriptBytes; + this.outpoint = outpoint; + this.sequence = 0xFFFFFFFFL; + this.parentTransaction = parentTransaction; + + length = 40 + (scriptBytes == null ? 1 : VarInt.sizeOf(scriptBytes.length) + scriptBytes.length); + } + /** * Creates an UNSIGNED input that links to the given output */ diff --git a/src/com/google/bitcoin/core/TransactionOutPoint.java b/src/com/google/bitcoin/core/TransactionOutPoint.java index f83e6233..a1251c2f 100644 --- a/src/com/google/bitcoin/core/TransactionOutPoint.java +++ b/src/com/google/bitcoin/core/TransactionOutPoint.java @@ -38,7 +38,7 @@ public class TransactionOutPoint extends ChildMessage implements Serializable { // It points to the connected transaction. Transaction fromTx; - TransactionOutPoint(NetworkParameters params, long index, Transaction fromTx) { + public TransactionOutPoint(NetworkParameters params, long index, Transaction fromTx) { super(params); this.index = index; if (fromTx != null) { diff --git a/src/com/google/bitcoin/core/TransactionOutput.java b/src/com/google/bitcoin/core/TransactionOutput.java index afbe224c..65cacab9 100644 --- a/src/com/google/bitcoin/core/TransactionOutput.java +++ b/src/com/google/bitcoin/core/TransactionOutput.java @@ -92,6 +92,15 @@ public class TransactionOutput extends ChildMessage implements Serializable { length = 8 + VarInt.sizeOf(scriptBytes.length) + scriptBytes.length; } + public TransactionOutput(NetworkParameters params, Transaction parent, BigInteger value, byte[] scriptBytes) { + super(params); + this.value = value; + this.scriptBytes = scriptBytes; + parentTransaction = parent; + availableForSpending = true; + length = 8 + VarInt.sizeOf(scriptBytes.length) + scriptBytes.length; + } + /** * Used only in creation of the genesis blocks and in unit tests. */ @@ -153,7 +162,7 @@ public class TransactionOutput extends ChildMessage implements Serializable { * Sets this objects availableToSpend flag to false and the spentBy pointer to the given input. * If the input is null, it means this output was signed over to somebody else rather than one of our own keys. */ - void markAsSpent(TransactionInput input) { + public void markAsSpent(TransactionInput input) { assert availableForSpending; availableForSpending = false; spentBy = input; diff --git a/src/com/google/bitcoin/core/WalletTransaction.java b/src/com/google/bitcoin/core/WalletTransaction.java index ddf53a83..8d5814e7 100644 --- a/src/com/google/bitcoin/core/WalletTransaction.java +++ b/src/com/google/bitcoin/core/WalletTransaction.java @@ -16,6 +16,7 @@ package com.google.bitcoin.core; + /** * A Transaction in a Wallet - includes the pool ID * @@ -38,6 +39,17 @@ public class WalletTransaction { public int getValue() { return value; } + + public static Pool valueOf(int value) { + switch (value) { + case 0: return UNSPENT; + case 1: return SPENT; + case 2: return PENDING; + case 3: return INACTIVE; + case 4: return DEAD; + default: return null; + } + } } private Transaction transaction; private Pool pool; diff --git a/src/com/google/bitcoin/store/WalletProtobufSerializer.java b/src/com/google/bitcoin/store/WalletProtobufSerializer.java index 41657d5c..2c2f8a1e 100644 --- a/src/com/google/bitcoin/store/WalletProtobufSerializer.java +++ b/src/com/google/bitcoin/store/WalletProtobufSerializer.java @@ -16,10 +16,15 @@ package com.google.bitcoin.store; +import com.google.bitcoin.core.AddressFormatException; +import com.google.bitcoin.core.DumpedPrivateKey; import com.google.bitcoin.core.ECKey; +import com.google.bitcoin.core.NetworkParameters; +import com.google.bitcoin.core.Sha256Hash; import com.google.bitcoin.core.StoredBlock; import com.google.bitcoin.core.Transaction; import com.google.bitcoin.core.TransactionInput; +import com.google.bitcoin.core.TransactionOutPoint; import com.google.bitcoin.core.TransactionOutput; import com.google.bitcoin.core.Wallet; import com.google.bitcoin.core.WalletTransaction; @@ -28,7 +33,12 @@ import com.google.protobuf.ByteString; 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. @@ -36,7 +46,14 @@ import java.io.OutputStream; * @author Miron Cuperman */ public class WalletProtobufSerializer { - void writeWallet(Wallet wallet, OutputStream output) throws IOException { + // Used for de-serialization + private Map txMap; + + public WalletProtobufSerializer() { + txMap = new HashMap(); + } + + static public void writeWallet(Wallet wallet, OutputStream output) throws IOException { Protos.Wallet.Builder walletBuilder = Protos.Wallet.newBuilder(); walletBuilder .setNetworkIdentifier(wallet.getNetworkParameters().getId()) @@ -59,14 +76,16 @@ public class WalletProtobufSerializer { walletBuilder.build().writeTo(output); } - - private Protos.Wallet.Transaction makeTxProto(WalletTransaction wtx) { + + private static 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())); + .setPool(Protos.Wallet.Transaction.Pool.valueOf(wtx.getPool().getValue())) + .setHash(ByteString.copyFrom(tx.getHash().getBytes())) + ; // Handle inputs for (TransactionInput input : tx.getInputs()) { @@ -81,15 +100,17 @@ public class WalletProtobufSerializer { // Handle outputs for (TransactionOutput output : tx.getOutputs()) { - final TransactionInput spentBy = output.getSpentBy(); - txBuilder.addTransactionOutput( + Protos.Wallet.Transaction.TransactionOutput.Builder outputBuilder = 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()) - ); + .setScriptBytes(ByteString.copyFrom(output.getScriptBytes())) + .setValue(output.getValue().longValue()); + final TransactionInput spentBy = output.getSpentBy(); + if (spentBy != null) { + outputBuilder + .setSpentByTransactionHash(ByteString.copyFrom(spentBy.getHash().getBytes())) + .setSpentByTransactionIndex((int)spentBy.getOutpoint().getIndex()); // FIXME + } + txBuilder.addTransactionOutput(outputBuilder); } // Handle which blocks tx was seen in @@ -99,4 +120,100 @@ public class WalletProtobufSerializer { return txBuilder.build(); } + + static public Wallet readWallet(InputStream input, NetworkParameters params, BlockStore store) + throws IOException, AddressFormatException, BlockStoreException { + WalletProtobufSerializer serializer = new WalletProtobufSerializer(); + Protos.Wallet walletProto = Protos.Wallet.parseFrom(input); + if (!params.getId().equals(walletProto.getNetworkIdentifier())) + throw new IllegalArgumentException( + "Trying to read a wallet with a different network id " + + walletProto.getNetworkIdentifier()); + + Wallet wallet = new Wallet(params); + + // Read all keys + for (Protos.Wallet.Key keyProto : walletProto.getKeyList()) { + wallet.addKey(new DumpedPrivateKey(params, keyProto.getPrivateKey()).getKey()); + } + + // Read all transactions and create outputs + for (Protos.Wallet.Transaction txProto : walletProto.getTransactionList()) { + serializer.readTransaction(txProto, params, store); + } + + // Create transactions inputs pointing to transactions + for (Protos.Wallet.Transaction txProto : walletProto.getTransactionList()) { + serializer.connectTransactionInputs(txProto, params); + } + + // Update transaction outputs to point to inputs that spend them + for (Protos.Wallet.Transaction txProto : walletProto.getTransactionList()) { + WalletTransaction wtx = serializer.connectTransactionOutputs(txProto, params); + wallet.addWalletTransaction(wtx); + } + + return wallet; + } + + + private void readTransaction(Protos.Wallet.Transaction txProto, + NetworkParameters params, BlockStore store) throws BlockStoreException { + Transaction tx = new Transaction(params, new Sha256Hash(txProto.getHash().toByteArray())); + if (txProto.hasUpdatedAt()) + tx.setUpdateTime(new Date(txProto.getUpdatedAt())); + + for (Protos.Wallet.Transaction.TransactionOutput outputProto : + txProto.getTransactionOutputList()) { + TransactionOutput output = new TransactionOutput(params, tx, + BigInteger.valueOf(outputProto.getValue()), + outputProto.getScriptBytes().toByteArray()); + tx.addOutput(output); + } + + if (txMap.containsKey(tx.getHash())) { + throw new RuntimeException("Transaction " + tx.getHashAsString() + " appears twice"); + } + + for (ByteString blockHash : txProto.getBlockHashList()) { + tx.addBlockAppearance(store.get(new Sha256Hash(blockHash.toByteArray())), false); + } + + txMap.put(txProto.getHash(), tx); + } + + private void connectTransactionInputs(Protos.Wallet.Transaction txProto, NetworkParameters params) { + Transaction tx = txMap.get(txProto.getHash()); + for (Protos.Wallet.Transaction.TransactionInput transactionInput : txProto.getTransactionInputList()) { + TransactionInput input = + new TransactionInput(params, tx, + transactionInput.getScriptBytes().toByteArray(), + new TransactionOutPoint(params, + transactionInput.getTransactionOutPointIndex(), + txMap.get(transactionInput.getTransactionOutPointHash()) + ) + ); + tx.addInput(input); + } + } + + private WalletTransaction connectTransactionOutputs( + org.bitcoinj.wallet.Protos.Wallet.Transaction txProto, NetworkParameters params) { + Transaction tx = txMap.get(txProto.getHash()); + WalletTransaction.Pool pool = + WalletTransaction.Pool.valueOf(txProto.getPool().getNumber()); + for (int i = 0 ; i < tx.getOutputs().size() ; i++) { + TransactionOutput output = tx.getOutputs().get(i); + final Protos.Wallet.Transaction.TransactionOutput transactionOutput = + txProto.getTransactionOutput(i); + if (transactionOutput.hasSpentByTransactionHash()) { + Transaction spendingTx = + txMap.get(transactionOutput.getSpentByTransactionHash()); + final int spendingIndex = transactionOutput.getSpentByTransactionIndex(); + output.markAsSpent(spendingTx.getInputs().get(spendingIndex)); + } + } + + return new WalletTransaction(pool, tx); + } }