Protobuf serialization for Wallet

This commit is contained in:
Miron Cuperman
2012-01-06 14:50:34 -08:00
committed by Miron Cuperman
parent 0e7e583626
commit 6af16c863c
9 changed files with 399 additions and 31 deletions

80
src/bitcoin.proto Normal file
View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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<StoredBlock> getAppearsIn() {
public Set<StoredBlock> 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<StoredBlock>();
}

View File

@@ -205,7 +205,7 @@ public class TransactionOutput extends ChildMessage implements Serializable {
/**
* Returns the connected input.
*/
TransactionInput getSpentBy() {
public TransactionInput getSpentBy() {
return spentBy;
}

View File

@@ -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<Sha256Hash, Transaction>();
eventListeners = new ArrayList<WalletEventListener>();
}
public NetworkParameters getNetworkParameters() {
return params;
}
public Iterable<ECKey> 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<WalletTransaction> getWalletTransactions() {
Set<WalletTransaction> all = new HashSet<WalletTransaction>();
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<WalletTransaction> txs,
Pool poolType, Map<Sha256Hash, Transaction> 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<Transaction> 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<Pool> getContainingPools(Transaction tx) {
EnumSet<Pool> 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<Sha256Hash, Transaction> pool, Transaction tx) {

View File

@@ -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;
}
}

View File

@@ -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();
}
}