3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-12 02:05:53 +00:00

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

56
pom.xml
View File

@ -213,6 +213,54 @@
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>compile-protoc</id>
<phase>generate-sources</phase>
<configuration>
<tasks>
<mkdir dir="${generated.sourceDirectory}" />
<path id="proto.path">
<fileset dir="src">
<include name="**/*.proto" />
</fileset>
</path>
<pathconvert pathsep=" " property="proto.files" refid="proto.path" />
<exec executable="protoc" failonerror="true">
<arg value="--java_out=${generated.sourceDirectory}" />
<arg value="-I${project.basedir}/src" />
<arg line="${proto.files}" />
</exec>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.1</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>gen</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
@ -260,6 +308,12 @@
<artifactId>bcprov-jdk15</artifactId>
<version>${bcprov-jdk15.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf.version}</version>
</dependency>
</dependencies>
<properties>
@ -270,6 +324,8 @@
<junit.version>4.8.2</junit.version>
<slf4j.version>1.6.2</slf4j.version>
<derby.version>10.8.2.2</derby.version>
<protobuf.version>2.2.0</protobuf.version>
<generated.sourceDirectory>gen</generated.sourceDirectory>
</properties>
</project>

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

@ -79,6 +79,8 @@ public class NetworkParameters implements Serializable {
*/
public byte[] alertSigningKey;
public String id;
private static Block createGenesis(NetworkParameters n) {
Block genesisBlock = new Block(n);
Transaction t = new Transaction(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;
@ -151,6 +153,14 @@ public class Wallet implements Serializable {
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.
*/
@ -186,6 +196,12 @@ public class Wallet implements Serializable {
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();
}
}

View File

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