3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-07 23:03:04 +00:00

Wallet: change the extensions API.

The old API was very limiting. Rather than have extensions be a single object that creates subclasses of the Wallet class, allow arbitrary objects to be attached to Wallets. Objects know if they are mandatory and how to serialize themselves. A wallet can have as many extensions as wanted in this way.
This commit is contained in:
Mike Hearn 2013-05-06 15:40:36 +02:00
parent 3eb3dbcf89
commit c64453f835
5 changed files with 197 additions and 207 deletions

View File

@ -16,27 +16,22 @@
package com.google.bitcoin.core;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.util.concurrent.SettableFuture;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.spongycastle.crypto.params.KeyParameter;
import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.bitcoin.core.WalletTransaction.Pool;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.bitcoin.store.WalletProtobufSerializer;
import com.google.bitcoin.utils.Locks;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.*;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import java.io.*;
import java.math.BigInteger;
@ -249,6 +244,9 @@ public class Wallet implements Serializable, BlockChainListener {
private int version;
// User-provided description that may help people keep track of what a wallet is for.
private String description;
// Stores objects that know how to serialize/unserialize themselves to byte streams and whether they're mandatory
// or not. The string key comes from the extension itself.
private final HashMap<String, WalletExtension> extensions;
/**
* Creates a new, empty wallet with no keys and no transactions. If you want to restore a wallet from disk instead,
@ -270,6 +268,7 @@ public class Wallet implements Serializable, BlockChainListener {
pending = new HashMap<Sha256Hash, Transaction>();
dead = new HashMap<Sha256Hash, Transaction>();
eventListeners = new CopyOnWriteArrayList<WalletEventListener>();
extensions = new HashMap<String, WalletExtension>();
createTransientState();
}
@ -2847,6 +2846,37 @@ public class Wallet implements Serializable, BlockChainListener {
return future;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Extensions to the wallet format.
/**
* By providing an object implementing the {@link WalletExtension} interface, you can save and load arbitrary
* additional data that will be stored with the wallet. Each extension is identified by an ID, so attempting to
* add the same extension twice (or two different objects that use the same ID) will throw an IllegalStateException.
*/
public void addExtension(WalletExtension extension) {
String id = checkNotNull(extension).getWalletExtensionID();
lock.lock();
try {
if (extensions.containsKey(id))
throw new IllegalStateException("Cannot add two extensions with the same ID: " + id);
extensions.put(id, extension);
} finally {
lock.unlock();
}
}
/** Returns a snapshot of all registered extension objects. The extensions themselves are not copied. */
public Map<String, WalletExtension> getExtensions() {
lock.lock();
try {
return ImmutableMap.copyOf(extensions);
} finally {
lock.unlock();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Boilerplate for running event listeners - unlocks the wallet, runs, re-locks.

View File

@ -0,0 +1,44 @@
/*
* Copyright 2013 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;
/**
* <p>An object implementing this interface can be added to a {@link Wallet} and provide arbitrary byte arrays that will
* be serialized alongside the wallet. Extensions can be mandatory, in which case applications that don't know how to
* read the given data will refuse to load the wallet at all. Extensions identify themselves with a string ID that
* should use a Java-style reverse DNS identifier to avoid being mixed up with other kinds of extension. To use an
* extension, add an object that implements this interface to the wallet using {@link Wallet#addExtension(WalletExtension)}
* before you load it (to read existing data) and ensure it's present when the wallet is save (to write the data).</p>
*
* <p>Note that extensions are singletons - you cannot add two objects that provide the same ID to the same wallet.</p>
*/
public interface WalletExtension {
/** Returns a Java package/class style name used to disambiguate this extension from others. */
public String getWalletExtensionID();
/**
* If this returns true, the mandatory flag is set when the wallet is serialized and attempts to load it without
* the extension being in the wallet will throw an exception. This method should not change its result during
* the objects lifetime.
*/
public boolean isWalletExtensionMandatory();
/** Returns bytes that will be saved in the wallet. */
public byte[] serializeWalletExtension();
/** Loads the contents of this object from the wallet. */
public void deserializeWalletExtension(byte[] data);
}

View File

@ -1,54 +0,0 @@
/*
* 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 java.util.Collection;
import java.util.Collections;
import org.bitcoinj.wallet.Protos;
import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.Wallet;
import com.google.bitcoin.crypto.KeyCrypter;
/**
* Optional helper for WalletProtobufSerializer that allows for serialization and deserialization of Wallet objects
* with extensions and corresponding extended Wallet classes. If you want to store proprietary data into the wallet,
* this is how to do it.
*/
public class WalletExtensionSerializer {
public Wallet newWallet(NetworkParameters params) {
return new Wallet(params);
}
public Wallet newWallet(NetworkParameters params, KeyCrypter keyCrypter) {
return new Wallet(params, keyCrypter);
}
public void readExtension(Wallet wallet, Protos.Extension extProto) {
if (extProto.getMandatory()) {
throw new IllegalArgumentException("Unknown mandatory extension in the wallet: " + extProto.getId());
}
}
/**
* Get collection of extensions to add, should be overridden by any class adding wallet extensions.
*/
public Collection<Protos.Extension> getExtensionsToWrite(Wallet wallet) {
return Collections.emptyList();
}
}

View File

@ -16,6 +16,19 @@
package com.google.bitcoin.store;
import com.google.bitcoin.core.*;
import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.bitcoin.crypto.EncryptedPrivateKey;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.common.base.Preconditions;
import com.google.protobuf.ByteString;
import com.google.protobuf.TextFormat;
import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -24,31 +37,6 @@ import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import com.google.bitcoin.crypto.EncryptedPrivateKey;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.PeerAddress;
import com.google.bitcoin.core.Sha256Hash;
import com.google.bitcoin.core.Transaction;
import com.google.bitcoin.core.TransactionConfidence;
import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
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;
import com.google.common.base.Preconditions;
import com.google.protobuf.ByteString;
import com.google.protobuf.TextFormat;
import static com.google.common.base.Preconditions.checkNotNull;
/**
@ -75,19 +63,9 @@ public class WalletProtobufSerializer {
// Used for de-serialization
protected Map<ByteString, Transaction> txMap;
protected WalletExtensionSerializer helper;
public WalletProtobufSerializer() {
txMap = new HashMap<ByteString, Transaction>();
helper = new WalletExtensionSerializer();
}
/**
* Set the WalletExtensionSerializer used to create new wallet objects
* and handle extensions
*/
public void setWalletExtensionSerializer(WalletExtensionSerializer h) {
this.helper = h;
}
/**
@ -187,18 +165,25 @@ public class WalletProtobufSerializer {
}
}
populateExtensions(wallet, walletBuilder);
// Populate the wallet version.
walletBuilder.setVersion(wallet.getVersion());
Collection<Protos.Extension> extensions = helper.getExtensionsToWrite(wallet);
for(Protos.Extension ext : extensions) {
walletBuilder.addExtension(ext);
}
return walletBuilder.build();
}
protected static Protos.Transaction makeTxProto(WalletTransaction wtx) {
private static void populateExtensions(Wallet wallet, Protos.Wallet.Builder walletBuilder) {
for (WalletExtension extension : wallet.getExtensions().values()) {
Protos.Extension.Builder proto = Protos.Extension.newBuilder();
proto.setId(extension.getWalletExtensionID());
proto.setMandatory(extension.isWalletExtensionMandatory());
proto.setData(ByteString.copyFrom(extension.serializeWalletExtension()));
walletBuilder.addExtension(proto);
}
}
private static Protos.Transaction makeTxProto(WalletTransaction wtx) {
Transaction tx = wtx.getTransaction();
Protos.Transaction.Builder txBuilder = Protos.Transaction.newBuilder();
@ -257,7 +242,7 @@ public class WalletProtobufSerializer {
return txBuilder.build();
}
protected static void writeConfidence(Protos.Transaction.Builder txBuilder,
private static void writeConfidence(Protos.Transaction.Builder txBuilder,
TransactionConfidence confidence,
Protos.TransactionConfidence.Builder confidenceBuilder) {
synchronized (confidence) {
@ -309,19 +294,17 @@ public class WalletProtobufSerializer {
}
/**
* Parses a wallet from the given stream. The stream is expected to contain a binary serialization of a
* {@link Protos.Wallet} object.<p>
*
* @throws IOException if there is a problem reading the stream.
* @throws IllegalArgumentException if the wallet is corrupt.
* Parses a wallet from the given stream, using the provided Wallet instance to load data into. This is primarily
* used when you want to register extensions. Data in the proto will be added into the wallet where applicable and
* overwrite where not.
*/
public Wallet readWallet(InputStream input) throws IOException {
// TODO: This method should throw more specific exception types than IllegalArgumentException.
Protos.Wallet walletProto = parseToProto(input);
// System.out.println(TextFormat.printToString(walletProto));
// Read the scrypt parameters that specify how encryption and decryption is performed.
// TODO: Why is the key crypter special? This should just be added to the wallet after construction as well.
KeyCrypter keyCrypter = null;
if (walletProto.hasEncryptionParameters()) {
Protos.ScryptParameters encryptionParameters = walletProto.getEncryptionParameters();
@ -329,8 +312,21 @@ public class WalletProtobufSerializer {
}
NetworkParameters params = NetworkParameters.fromID(walletProto.getNetworkIdentifier());
Wallet wallet = helper.newWallet(params, keyCrypter);
Wallet wallet = new Wallet(params, keyCrypter);
readWallet(walletProto, wallet);
return wallet;
}
/**
* Loads wallet data from the given protocol buffer and inserts it into the given Wallet object. This is primarily
* useful when you wish to pre-register extension objects. Note that if loading fails the provided Wallet object
* may be in an indeterminate state and should be thrown away.
*
* @throws IOException if there is a problem reading the stream.
* @throws IllegalArgumentException if the wallet is corrupt.
*/
public void readWallet(Protos.Wallet walletProto, Wallet wallet) throws IOException {
// TODO: This method should throw more specific exception types than IllegalArgumentException.
if (walletProto.hasDescription()) {
wallet.setDescription(walletProto.getDescription());
}
@ -352,6 +348,7 @@ public class WalletProtobufSerializer {
byte[] pubKey = keyProto.hasPublicKey() ? keyProto.getPublicKey().toByteArray() : null;
ECKey ecKey;
final KeyCrypter keyCrypter = wallet.getKeyCrypter();
if (keyCrypter != null && keyCrypter.getUnderstoodEncryptionType() != EncryptionType.UNENCRYPTED) {
// If the key is encrypted construct an ECKey using the encrypted private key bytes.
ecKey = new ECKey(encryptedPrivateKey, pubKey, keyCrypter);
@ -365,7 +362,7 @@ public class WalletProtobufSerializer {
// Read all transactions and insert into the txMap.
for (Protos.Transaction txProto : walletProto.getTransactionList()) {
readTransaction(txProto, params);
readTransaction(txProto, wallet.getParams());
}
// Update transaction outputs to point to inputs that spend them
@ -386,9 +383,7 @@ public class WalletProtobufSerializer {
wallet.setLastBlockSeenHeight(walletProto.getLastSeenBlockHeight());
}
for (Protos.Extension extProto : walletProto.getExtensionList()) {
helper.readExtension(wallet, extProto);
}
loadExtensions(wallet, walletProto);
if (walletProto.hasVersion()) {
wallet.setVersion(walletProto.getVersion());
@ -396,8 +391,22 @@ public class WalletProtobufSerializer {
// Make sure the object can be re-used to read another wallet without corruption.
txMap.clear();
}
return wallet;
private static void loadExtensions(Wallet wallet, Protos.Wallet walletProto) {
final Map<String, WalletExtension> extensions = wallet.getExtensions();
for (Protos.Extension extProto : walletProto.getExtensionList()) {
String id = extProto.getId();
WalletExtension extension = extensions.get(id);
if (extension == null) {
if (extProto.getMandatory()) {
throw new IllegalArgumentException("Unknown mandatory extension in wallet: " + id);
}
} else {
log.info("Loading wallet extension {}", id);
extension.deserializeWalletExtension(extProto.getData().toByteArray());
}
}
}
/**
@ -409,7 +418,7 @@ public class WalletProtobufSerializer {
return Protos.Wallet.parseFrom(input);
}
protected void readTransaction(Protos.Transaction txProto, NetworkParameters params) {
private void readTransaction(Protos.Transaction txProto, NetworkParameters params) {
Transaction tx = new Transaction(params);
if (txProto.hasUpdatedAt()) {
tx.setUpdateTime(new Date(txProto.getUpdatedAt()));
@ -452,7 +461,7 @@ public class WalletProtobufSerializer {
txMap.put(txProto.getHash(), tx);
}
protected WalletTransaction connectTransactionOutputs(org.bitcoinj.wallet.Protos.Transaction txProto) {
private WalletTransaction connectTransactionOutputs(org.bitcoinj.wallet.Protos.Transaction txProto) {
Transaction tx = txMap.get(txProto.getHash());
WalletTransaction.Pool pool = WalletTransaction.Pool.valueOf(txProto.getPool().getNumber());
if (pool == WalletTransaction.Pool.INACTIVE || pool == WalletTransaction.Pool.PENDING_INACTIVE) {
@ -486,7 +495,7 @@ public class WalletProtobufSerializer {
return new WalletTransaction(pool, tx);
}
protected void readConfidence(Transaction tx, Protos.TransactionConfidence confidenceProto,
private void readConfidence(Transaction tx, Protos.TransactionConfidence confidenceProto,
TransactionConfidence confidence) {
// We are lenient here because tx confidence is not an essential part of the wallet.
// If the tx has an unknown type of confidence, ignore.

View File

@ -6,22 +6,20 @@ 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.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.InetAddress;
import java.util.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Set;
import static com.google.bitcoin.core.TestUtils.createFakeTx;
import static org.junit.Assert.*;
import com.google.bitcoin.crypto.KeyCrypter;
public class WalletProtobufSerializerTest {
static final NetworkParameters params = NetworkParameters.unitTests();
private ECKey myKey;
@ -227,7 +225,7 @@ public class WalletProtobufSerializerTest {
assertEquals(work2, rebornConfidence1.getWorkDone());
}
private Wallet roundTrip(Wallet wallet) throws Exception {
private static Wallet roundTrip(Wallet wallet) throws Exception {
ByteArrayOutputStream output = new ByteArrayOutputStream();
//System.out.println(WalletProtobufSerializer.walletToText(wallet));
new WalletProtobufSerializer().writeWallet(wallet, output);
@ -237,7 +235,7 @@ public class WalletProtobufSerializerTest {
@Test
public void testSerializedExtensionNormalWallet() throws Exception {
public void testRoundTripNormalWallet() throws Exception {
Wallet wallet1 = roundTrip(myWallet);
assertEquals(0, wallet1.getTransactions(true).size());
assertEquals(BigInteger.ZERO, wallet1.getBalance());
@ -251,99 +249,62 @@ public class WalletProtobufSerializerTest {
}
@Test
public void testSerializedExtensionFancyWallet() throws Exception {
Random rnd = new Random();
WalletExtension wallet1 = new WalletExtension(params);
wallet1.addKey(myKey);
wallet1.random_bytes = new byte[100];
rnd.nextBytes(wallet1.random_bytes);
Wallet wallet2 = roundTripExtension(wallet1);
assertTrue("Wallet2 is not an instance of WalletExtension. It is a " + wallet2.getClass().getCanonicalName(), wallet2 instanceof WalletExtension);
WalletExtension wallet2ext = (WalletExtension)wallet2;
assertNotNull("Wallet2s random bytes were null", wallet2ext.random_bytes);
for (int i = 0; i < 100; i++) {
assertEquals("Wallet extension byte different at byte " + i, wallet1.random_bytes[i], wallet2ext.random_bytes[i]);
public void testExtensions() throws Exception {
myWallet.addExtension(new SomeFooExtension("com.whatever.required", true));
Protos.Wallet proto = new WalletProtobufSerializer().walletToProto(myWallet);
Wallet wallet2 = new Wallet(params);
// Initial extension is mandatory: try to read it back into a wallet that doesn't know about it.
try {
new WalletProtobufSerializer().readWallet(proto, wallet2);
fail();
} catch (IllegalArgumentException e) {
// Expected.
}
Wallet wallet3 = new Wallet(params);
// This time it works.
wallet3.addExtension(new SomeFooExtension("com.whatever.required", true));
new WalletProtobufSerializer().readWallet(proto, wallet3);
assertTrue(wallet3.getExtensions().containsKey("com.whatever.required"));
// Non-mandatory extensions are ignored if the wallet doesn't know how to read them.
Wallet wallet4 = new Wallet(params);
wallet4.addExtension(new SomeFooExtension("com.whatever.optional", false));
Protos.Wallet proto4 = new WalletProtobufSerializer().walletToProto(wallet4);
Wallet wallet5 = new Wallet(params);
new WalletProtobufSerializer().readWallet(proto4, wallet5);
assertEquals(0, wallet5.getExtensions().size());
}
@Test
public void testSerializedExtensionFancyWalletRegularTrip() throws Exception {
Random rnd = new Random();
WalletExtension wallet1 = new WalletExtension(params);
wallet1.addKey(myKey);
wallet1.random_bytes=new byte[100];
rnd.nextBytes(wallet1.random_bytes);
private static class SomeFooExtension implements WalletExtension {
private final byte[] data = new byte[]{1, 2, 3};
Wallet wallet2 = roundTrip(myWallet);
assertFalse(wallet2 instanceof WalletExtension);
private final boolean isMandatory;
private final String id;
}
private Wallet roundTripExtension(Wallet wallet) throws Exception {
ByteArrayOutputStream output = new ByteArrayOutputStream();
WalletProtobufSerializer serializer = new WalletProtobufSerializer();
serializer.setWalletExtensionSerializer(new WalletExtensionSerializerRandom());
serializer.writeWallet(wallet, output);
ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
return serializer.readWallet(input);
}
/**
* An extension of a wallet that stores a number.
*/
public static class WalletExtension extends Wallet {
public byte[] random_bytes;
public WalletExtension(NetworkParameters params) {
super(params);
}
}
public static class WalletExtensionSerializerRandom extends WalletExtensionSerializer {
@Override
public Collection<Protos.Extension> getExtensionsToWrite(Wallet wallet) {
List<Protos.Extension> lst = new LinkedList<Protos.Extension>();
if (wallet instanceof WalletExtension) {
WalletExtension walletExt = (WalletExtension) wallet;
Protos.Extension.Builder e = Protos.Extension.newBuilder();
e.setId("WalletExtension.random_bytes");
e.setMandatory(false);
e.setData(ByteString.copyFrom(walletExt.random_bytes));
lst.add(e.build());
}
lst.addAll(super.getExtensionsToWrite(wallet));
return lst;
public SomeFooExtension(String id, boolean isMandatory) {
this.isMandatory = isMandatory;
this.id = id;
}
@Override
public Wallet newWallet(NetworkParameters params) {
return new WalletExtension(params);
public String getWalletExtensionID() {
return id;
}
@Override
public Wallet newWallet(NetworkParameters params, KeyCrypter keyCrypter) {
// Ignore encryption.
return new WalletExtension(params);
public boolean isWalletExtensionMandatory() {
return isMandatory;
}
@Override
public void readExtension(Wallet wallet, Protos.Extension extProto) {
if (wallet instanceof WalletExtension) {
WalletExtension walletExt = (WalletExtension) wallet;
if (extProto.getId().equals("WalletExtension.random_bytes")) {
walletExt.random_bytes = extProto.getData().toByteArray();
return;
}
}
super.readExtension(wallet, extProto);
public byte[] serializeWalletExtension() {
return data;
}
@Override
public void deserializeWalletExtension(byte[] data) {
assertArrayEquals(this.data, data);
}
}
}